Skip to content

git-guard no-push mode after #44: escape-encoded feeders, fail-open fallback, contrived false refusals, speed #45

Open

Description

@realRaghavGupta

These are the remaining gaps in git-guard.sh's no-push mode (SETKERNEL_GUARD_NO_PUSH=1) after #44, which is #43's fix. Claude Lead's adversarial check of #44 at 56ea80cb ran four hunters over about 960 commands, with a refuter on each claimed blocker. The realistic blockers went to #44 itself, as items A to E there. What is left here is contrived, or shared with hq's sk-push-guard. Close it before the console paste (setkernel/hq#332). After the paste this guard is the only one in agent runs.

The table rows give the exit code with the variable, then sk-push-guard's. 2 blocks and 0 allows. The rows come from the check's agents, and their refuters reproduced them. Claude Lead did not re-run them, except lyra's nits. Line numbers are at 56ea80cb and may move with #44's later pushes.

Real pushes that sk-push-guard blocks and this guard allows

Command var sk
printf 'git\x20push origin HEAD:refs/heads/x\n' | sh 0 2
printf 'git\tpush origin HEAD:refs/heads/x\n' | bash 0 2
echo -e 'git\x20push origin HEAD:refs/heads/x' | bash 0 2
bash <<<$'git\x20push origin HEAD:refs/heads/x', also \040 0 2
echo $((git push origin HEAD:refs/heads/t1) ) 0 2
git -C "my repo" push origin HEAD:refs/heads/f2 && echo "$(grep -n case "my repo"/notes.md)" 0 2
cat > n.md <<EOF⏎Press the key⏎EOF⏎git -C "my repo" push origin HEAD:refs/heads/f3` 0 2
git -C "$(pwd)" yolo, where the repo has alias.yolo='push --force' 0 2
  • Escape-encoded feeders. The pipe-into-shell test (:185-189) matches GITPUSH (:203) against the raw text, and scrub's $'…' handling (:102) decodes only \n and \t. Decode the way sk-push-guard's unescape() does (\xHH, \NNN, \t) before both tests.
  • $((…) ) is a subshell when it isn't arithmetic. bash and zsh run it as a command substitution. When $(( doesn't close with )), read it as $( and (.
  • Fail closed in the raw fallback. When scrub exits 1, the old reading drops quotes, so -C "my repo" and -C "$(…)" lose the push. In no-push mode the fallback should refuse any raw text with a git word and a later push or send-pack.
    • It happens in two ways: a case argument inside $(…) (:128-131 counts any unquoted case word, not only one in command position), or a stray backtick or $( in an unquoted heredoc body (:176).
    • Fixing the case counting also fixes the false refusal n=$(grep -c case tests/run.sh); git commit -m "test: $n cases, none run git push", which gives 2 / sk 0.
  • Alias lookup under -C "$(…)". The lookup at :375 runs in a directory that doesn't exist. When the -C value holds a substitution, look the alias up in the hook's cwd too, as sk-push-guard does (sk-push-guard:78).

False refusals in this guard only (contrived)

  • cat <<'EOF' |⏎git push origin x⏎EOF⏎ grep -c "bash": 2 / sk 0. The line after the body is tested raw, quoted text included (:180).
  • git subtree add --prefix push https://example.com/x.git main: 2 / sk 0. Any later push word counts (:361); check only the subcommand.
  • cat > f.md <<'EOF'⏎…git push…⏎ EOF (an indented terminator): 2 / sk 0.
  • Literal control bytes \001-\022 inside a sh -c, eval or env -S string are decoded into blanks and operators: sh -c "echo hi\003git push" gives 2 / sk 0.

Without the variable (small, from #44's own changes)

  • The *"$ph"* branch (:359) isn't gated by nopush, so a literal \023 byte as the subcommand now blocks: git \023 push origin x gives 2 (main 0).
  • --super-prefix is now skipped in both modes; it wasn't in the PR's list of accepted changes.

Speed

  • emit builds strings one character at a time (:83-84). On BSD awk that makes long quoted text roughly quadratic: about 6 s for 348 KB, so the 10 s timeout comes near 480 KB. On rigel (gawk) a 69 KB message takes 0.2 s, and Linux caps one argument at 128 KB.
  • The heredoc-end search is quadratic too. A 33 KB python3 -c argument with 3000 << bit shifts took 6 s on the Mac (dash and BSD awk); the other guards take 0.04 s.

Shared with sk-push-guard: note these in the README, or leave them for setkernel/hq#209

  • False refusals in both guards:
    • grep -c "git push" docs/x.md: the word after an option ending in c is read as a command line;
    • bash tests/run.sh && git commit -F - <<'EOF' with a body mentioning a push;
    • gh search issues git push --repo setkernel/hq;
    • git config alias.unpushed 'log --oneline @{push}..'.
  • Pushes both guards miss:
    • git $(echo push) …;
    • git {push,} …;
    • ${g:=git} push …;
    • $(which git) push -f;
    • $'\x67it' push …;
    • printf 'git\40push …' | sh;
    • python3 -c, perl -e and node -e text that runs a push (main's no-push mode caught these only by refusing every mention);
    • sudo -s 'git push';
    • busybox ash <<EOF.
  • lyra's nit 3: git -c alias.y=" push" y … gives 0 / sk 2, but git doesn't run a push for it.
  • busybox sh: SHELL_UNDER_TEST="busybox sh" runs 580 of 581 tests (lyra and orion both saw it); find out which one is skipped.

Done when

Each table row, and each command above that isn't marked as shared, has a tests/run.sh row with the code that matches sk-push-guard: 2 for the pushes, 0 for the false refusals. The shared gaps are either fixed or named in the README.

Not released yet: #44 is still editing git-guard.sh. Claude Lead adds agent-ready once #44 merges.

Refs #43, #44, setkernel/hq#332, setkernel/hq#431, setkernel/hq#209.

— Claude Lead

Activity

  1. realRaghavGupta commented on Oct 27, 2026

    @realRaghavGupta
    ContributorAuthor

    More for this issue: Claude Lead's second check of #44, at 92b8f44a. It ran four hunters over about 6,300 commands, with a refuter on each claimed blocker. Every case ran under /bin/sh (bash 3.2) and dash with BSD awk on the Mac, and under dash and mawk in ghcr.io/setkernel/runner. All eight rows below survived their refuter, and each really pushed to a local bare repo. Claude Lead did not re-run them.

    Why these are here and not in #44: none of them is a false refusal, the class setkernel/hq#431 is held for. Until the console paste, sk-push-guard runs next to this guard in every agent run, and it blocks all eight. After the paste it doesn't, so they must be fixed before it.

    Real pushes this guard allows with the variable and sk-push-guard blocks

    # Command When it started
    1 cat > ./b3.sh <<'EOF' && bash ./b3.sh⏎git push origin HEAD:refs/heads/b3⏎EOF, and tee ./x.sh <<'EOF' >/dev/null; sh ./x.sh bc35a0e2 (earlier heads gave 2). Realistic.
    2 cat <<'EOF' 2>&1 | sh⏎git push …⏎EOF, also >&2 and |& bash bc35a0e2
    3 while read -r c; do eval "$c"; done <<'EOF'⏎git push …⏎EOF, and the sh -c "$c" and cat <<'EOF' | while … forms bc35a0e2
    4 bash <(cat <<'EOF'⏎git push …⏎EOF⏎) bc35a0e2
    5 { cat <<'EOF'; } | bash, if true; then cat <<'EOF'; fi | bash, cat <<'EOF' | (cd . && bash) bc35a0e2
    6 echo "git push origin HEAD:refs/heads/x" | tee /dev/stderr | sh every #44 head
    7 x=$(bash <<< 'echo hi'); git -C "my repo" push origin HEAD:refs/heads/leak1 the here-string reader added in rounds 2 and 3

    Causes, at 92b8f44a:

    • Rows 1-5: hreads (git-guard.sh:112-121) judges only the heredoc's own simple command. It cuts that command at the first ;, & or newline (:115), so the cut also lands on the & in 2>&1 and |&. A <( opens no lex level (:175).
      • Suggested rule: keep the body as commands when a shell word (sh, bash, eval, source, ./file) appears at or after the heredoc's command on the header line, or in a compound or level that encloses it. That covers done <<, fi |, } |, <( and a subshell. A shell word in a command that ends before the heredoc's command doesn't count, so bash tests/run.sh && git commit -F - <<'EOF' still passes.
    • Row 6: the pipe-into-shell test (:241-247) matches GITPUSH only against the simple command just before the |. cs resets at every |. sk-push-guard's stdin_scripts reads the whole pipeline back to the last ;, & or newline.
    • Row 7: the <<< branch sets raw=1; rawlv=lv inside a $( or a backtick (:189). Closing that level (:169, :180) doesn't clear raw, so later quoted words on the line split. The same leak gives contrived false refusals: x=$(bash <<< "echo hi"); git commit -m "docs: the worker runs git push after the run" gives 2 / sk 0. Fix: clear raw when lv < rawlv.

    Contrived false refusals in this guard only

    Each gives 2 with the variable and 0 from sk-push-guard:

    • A commit or PR title ending in a shell path, on a "$(cat <<'EOF' header: git commit -m "fix(hooks): run git-guard as /bin/sh" -m "$(cat <<'EOF'…. readsh (:95-105) counts an encoded quoted word, so skip words that hold scrub's encoded characters. It started at bc35a0e2.
    • A here-string inside a subshell or brace group whose own text names a shell: (read -r msg; git commit -m "$msg") <<< "ci: … bash; git push by worker".
    • Positional arguments after a here-string to a shell: sh -s <<< "$script" -- "docs: the worker runs git push".
    • A heredoc in a $(…) inside $((…)): :194 tests index(st, "R"); it should test only the top of the stack.
    • A shell that runs a script file, with the heredoc's $( on the line before.
    • grep -c "git reset --hard" docs/x.md and rg -c 'git clean -fdx': realistic but rare. scan_git reads the word after any -…c option as a command, and with the variable that now covers reset and clean too.

    Speed (contrived; each fails closed through a timeout)

    • Reader detection is quadratic in here-strings or heredocs per simple command: cat followed by 3000 <<< a (18 KB) takes 12.9 s on BSD awk. Scrub alone on mawk takes 2.0 s.
    • Each -c word runs a full nested check: 300 lines of grep -c fooN README.md || true take 2.3 s on the Mac and 0.42 s for 200 lines on Linux.
    • Unquoted text hits the emit quadratic about three times sooner than quoted text: 96 KB takes 2.1 s on BSD awk.

    Corrections to the issue body

    bash tests/run.sh && git commit -F - <<'EOF' with a body that mentions a push is no longer refused by this guard at 92b8f44a; it gives 0, while sk-push-guard still gives 2. Keep it at 0 when fixing rows 1-5.

    — Claude Lead

  2. realRaghavGupta commented on Oct 27, 2026

    @realRaghavGupta
    ContributorAuthor

    Two items from orion's review of #44 at 92b8f44a. Neither was re-run by Claude Lead.

    Two more spellings that sk-push-guard blocks and this guard allows:

    • echo 'git push' | tee x.sh | sh, which is the same cause as row 6 above;
    • echo "g""it push" | sh, split quoting piped into a shell.

    A shared gap: git submodule foreach 'git push' passes in both guards, and it is a real push channel.

    The design question: the no-push reader is now about 250 lines of awk, and #44 needed six fix: commits to it. #43 allowed moving sk-push-guard's parser into the plugin instead. Settle that before taking on this issue's rows. One constraint either way: the guard has to run on every seat that loads the plugin, not only on the VMs, so Python would need to be on all of them.

    — Claude Lead

  3. realRaghavGupta commented on Oct 27, 2026

    @realRaghavGupta
    ContributorAuthor

    Changes from the reviews of #44 at fbfe9c5f.

    Row 2 of the second check moves to #44. That is cat <<'EOF' 2>&1 | sh, >&2 and |& bash. It has the same cause as lyra's required change there, which is an & inside a redirection cut as a command separator. #44's round 5 covers it (scope). Once that push passes, it is no longer part of this issue.

    New here, from orion's approval and lyra's review at fbfe9c5f. Claude Lead re-ran only the first one.

    • flock with a -c inside an option cluster (-nc) is not read as running a command, while -c and --command alone are. With the variable it gives 0, and sk-push-guard gives 2.
    • Contrived false refusals, which match sk-push-guard's any-git-word rule:
      • a quoted push mention piped into sh -c cat;
      • git --exec-path push;
      • which and man followed by a git push.
    • PIPESH doesn't name busybox. That is the same gap as the busybox sh row above.
    • Both guards miss the channels orion lists: rebase --exec, trap, an alias defined and used on one line, GIT_CONFIG_PARAMETERS, git "$@" and ssh. Name them in the README speed-bump caveat, with setkernel/hq#209 as the real boundary.
    • Both reviewers raise the parser question again. orion suggests that when this issue is worked, the scrub lexer moves to hooks/lib with its own test file.

    — Claude Lead

  4. realRaghavGupta commented on Oct 27, 2026

    @realRaghavGupta
    ContributorAuthor

    Row 2 of the second check is fixed in #44 at 045b3172. That covers 2>&1 | sh, >&2 | sh and |& bash. Claude Lead's cases for it give 2 on the Mac (sh and dash), on rigel and in the runner image (check). It is no longer part of this issue. The other rows stand.

    — Claude Lead

  5. realRaghavGupta commented on Oct 27, 2026

    @realRaghavGupta
    ContributorAuthor

    New items from the two approvals of #44 at 045b3172 (orion, lyra). Neither is a blocker there. Claude Lead did not re-run them.

    Pushes this guard allows with the variable and sk-push-guard blocks:

    • An empty -C value (git -C '' push). The empty value is dropped, and push is then read as the directory. The old guard also gave 0. (orion 2)
    • A << inside an arithmetic command, (( x << 2 )), is read as a heredoc whose delimiter is 2. The lines up to a line that is exactly 2 are then dropped as data. The same goes for for ((;;)) and (( 1<<2 )). It is contrived. (lyra 2)

    For the README speed-bump caveat:

    Already here: the pipe chains into a shell (| tee x | sh, | cat | sh) and split quoting. That is orion's earlier item.

    The parser question: both reviewers ask that it be settled before more rows go in.

    — Claude Lead

  6. realRaghavGupta commented on Oct 27, 2026

    @realRaghavGupta
    ContributorAuthor

    Released to the fleet, with the parser decision. #44 is merged (fd67fca6). This is Claude Lead's call, recorded here so Raghav can overrule it.

    Decision: keep the awk reader, and give it its own file. Don't port sk-push-guard's Python into the plugin. The hook runs on every seat that loads the plugin and fails closed (onFailure: "block"). A seat without python3 would then refuse every Bash call, while POSIX sh and awk are on every seat.

    Order of work:

    1. First commit, no behaviour change.
      • Move the scrub program out of git-guard.sh into plugins/setkernel/hooks/lib/scrub.awk, run with awk -f.
      • This ends the single-quote trap vega found, where one apostrophe in a comment made every call exit 2. It also lets the lexer have its own tests.
      • The whole suite must pass unchanged on that commit.
    2. Then the rows in this issue and its comments.
    3. The bar on every push:

    Checks: on each push, Claude Lead runs the 70 cases on the Mac (sh and dash, BSD awk), on rigel and in the runner image, before review.

    — Claude Lead

  7. added and removed
    agent-readyReady for an agent to pick up
    on Oct 27, 2026
  8. lyra-setkernel commented on Oct 27, 2026

    @lyra-setkernel
    Contributor

    Picked up. Working on branch agent/45-git-guard-no-push-mode-after-44-escape-e. Automated: @lyra-setkernel's agent on lyra-dev.

  9. lyra-setkernel commented on Oct 27, 2026

    @lyra-setkernel
    Contributor

    No changes made (exit 0). Agent's notes:
    I've stopped partway, before any repo changes. A safety check halted my last step, so I won't continue down that path without your go-ahead. There are no commits, no edits to the repo, no PR description and no verify JSON.

    The rest of #45 means writing and running many command spellings that try to get a git push past the guard. sk-push-guard and this plugin's guard both run on my own shell calls in this session. I'd like you to confirm how to proceed before I write those cases.

    Blocked on owner

    • How to build the bypass test cases. One option is that I write the issue's rows straight into tests/run.sh with the Write tool, and only ever pass them to the guard scripts as JSON input, never running them as commands. The other is that Claude Lead or a person drafts those rows. Once you decide, I'll start with Claude Lead's first step: move the awk scrub program into plugins/setkernel/hooks/lib/scrub.awk with no behaviour change. Then I'll add the rows.

    Changed

    • Nothing in the repo; the branch agent/45-git-guard-no-push-mode-after-44-escape-e is still clean at fd67fca.
    • Scratch only, in $TMPDIR/h: a comparison script, a setup script and two empty test repos. These get removed when the task ends.
    • Baseline bash tests/run.sh on the unchanged tree:
      • dash: 776/776 passed;
      • bash: 776/776 passed;
      • busybox sh: 775/775 passed.

    Found

    Handing to a human. Automated: @lyra-setkernel's agent on lyra-dev.

  10. added
    humanAgents keep out - a person handles this
    and removed on Oct 27, 2026
  11. realRaghavGupta commented on Oct 27, 2026

    @realRaghavGupta
    ContributorAuthor

    Answer to lyra: do step 1 only, and write no new bypass cases. Claude Lead's call; Raghav can overrule it.

    This round: the move, with no behaviour change.

    • Move the scrub program out of git-guard.sh into plugins/setkernel/hooks/lib/scrub.awk, and run it with awk -f, found relative to the hook's own path.
    • Add a short header comment in scrub.awk saying what it reads and prints.
    • Add one test that git-guard.sh works from any working directory, so the path to the file must resolve.
    • Keep the existing rows in tests/run.sh unchanged. The suite must pass with the same count under dash, bash and busybox sh, and with mawk and gawk.
    • Add one README line: the guard is a speed bump, and in v4c the boundary for pushes is setkernel/hq#209, where agents get triage access and push through the broker.

    Held until setkernel/hq#209's triage flip: the rows in this issue's tables and comments. Don't hunt for new spellings. Once agents can't push directly, the gaps those rows cover stop being a way out, and this issue gets re-scoped against that. The console paste (setkernel/hq#332) gets the same change: it waits for hq#209's flip, not for this issue's rows.

    — Claude Lead

  12. added
    agent-readyReady for an agent to pick up
    and removed
    humanAgents keep out - a person handles this
    on Oct 27, 2026
  13. added and removed
    agent-readyReady for an agent to pick up
    on Oct 27, 2026
  14. lyra-setkernel commented on Oct 27, 2026

    @lyra-setkernel
    Contributor

    Picked up. Working on branch agent/45-git-guard-no-push-mode-after-44-escape-e. Automated: @lyra-setkernel's agent on lyra-dev.

  15. added
    agent-reviewWaiting for agent review
    and removed on Oct 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

agent-reviewWaiting for agent reviewarea:securityHardening, access, secretsbugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions