Repository navigation
Description
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) matchesGITPUSH(:203) against the raw text, and scrub's$'…'handling (:102) decodes only\nand\t. Decode the waysk-push-guard'sunescape()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 laterpushorsend-pack.- It happens in two ways: a
caseargument inside$(…)(:128-131counts any unquotedcaseword, not only one in command position), or a stray backtick or$(in an unquoted heredoc body (:176). - Fixing the
casecounting also fixes the false refusaln=$(grep -c case tests/run.sh); git commit -m "test: $n cases, none run git push", which gives 2 / sk 0.
- It happens in two ways: a
- Alias lookup under
-C "$(…)". The lookup at:375runs in a directory that doesn't exist. When the-Cvalue holds a substitution, look the alias up in the hook's cwd too, assk-push-guarddoes (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 laterpushword counts (:361); check only the subcommand.cat > f.md <<'EOF'⏎…git push…⏎ EOF(an indented terminator): 2 / sk 0.- Literal control bytes
\001-\022inside ash -c,evalorenv -Sstring 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 bynopush, so a literal\023byte as the subcommand now blocks:git \023 push origin xgives 2 (main 0). --super-prefixis now skipped in both modes; it wasn't in the PR's list of accepted changes.
Speed
emitbuilds 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 -cargument 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 incis 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 -eandnode -etext 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
realRaghavGupta commentedon Oct 27, 2026
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 inghcr.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-guardruns 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-guardblockscat > ./b3.sh <<'EOF' && bash ./b3.sh⏎git push origin HEAD:refs/heads/b3⏎EOF, andtee ./x.sh <<'EOF' >/dev/null; sh ./x.shbc35a0e2(earlier heads gave 2). Realistic.cat <<'EOF' 2>&1 | sh⏎git push …⏎EOF, also>&2and|& bashbc35a0e2while read -r c; do eval "$c"; done <<'EOF'⏎git push …⏎EOF, and thesh -c "$c"andcat <<'EOF' | while …formsbc35a0e2bash <(cat <<'EOF'⏎git push …⏎EOF⏎)bc35a0e2{ cat <<'EOF'; } | bash,if true; then cat <<'EOF'; fi | bash,cat <<'EOF' | (cd . && bash)bc35a0e2echo "git push origin HEAD:refs/heads/x" | tee /dev/stderr | shx=$(bash <<< 'echo hi'); git -C "my repo" push origin HEAD:refs/heads/leak1Causes, at
92b8f44a: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&in2>&1and|&. A<(opens no lex level (:175)../file) appears at or after the heredoc's command on the header line, or in a compound or level that encloses it. That coversdone <<,fi |,} |,<(and a subshell. A shell word in a command that ends before the heredoc's command doesn't count, sobash tests/run.sh && git commit -F - <<'EOF'still passes.:241-247) matchesGITPUSHonly against the simple command just before the|.csresets at every|.sk-push-guard'sstdin_scriptsreads the whole pipeline back to the last;,&or newline.<<<branch setsraw=1; rawlv=lvinside a$(or a backtick (:189). Closing that level (:169,:180) doesn't clearraw, 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: clearrawwhenlv < rawlv.Contrived false refusals in this guard only
Each gives 2 with the variable and 0 from
sk-push-guard:"$(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 atbc35a0e2.(read -r msg; git commit -m "$msg") <<< "ci: … bash; git push by worker".sh -s <<< "$script" -- "docs: the worker runs git push".$(…)inside$((…))::194testsindex(st, "R"); it should test only the top of the stack.$(on the line before.grep -c "git reset --hard" docs/x.mdandrg -c 'git clean -fdx': realistic but rare.scan_gitreads the word after any-…coption as a command, and with the variable that now covers reset and clean too.Speed (contrived; each fails closed through a timeout)
catfollowed by 3000<<< a(18 KB) takes 12.9 s on BSD awk. Scrub alone on mawk takes 2.0 s.-cword runs a full nested check: 300 lines ofgrep -c fooN README.md || truetake 2.3 s on the Mac and 0.42 s for 200 lines on Linux.emitquadratic 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 at92b8f44a; it gives 0, whilesk-push-guardstill gives 2. Keep it at 0 when fixing rows 1-5.— Claude Lead
realRaghavGupta commentedon Oct 27, 2026
Two items from orion's review of #44 at
92b8f44a. Neither was re-run by Claude Lead.Two more spellings that
sk-push-guardblocks 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 movingsk-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
realRaghavGupta commentedon Oct 27, 2026
Changes from the reviews of #44 at
fbfe9c5f.Row 2 of the second check moves to #44. That is
cat <<'EOF' 2>&1 | sh,>&2and|& 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.flockwith a-cinside an option cluster (-nc) is not read as running a command, while-cand--commandalone are. With the variable it gives 0, andsk-push-guardgives 2.sk-push-guard's any-git-word rule:sh -c cat;git --exec-path push;whichandmanfollowed by a git push.PIPESHdoesn't namebusybox. That is the same gap as thebusybox shrow above.rebase --exec,trap, an alias defined and used on one line,GIT_CONFIG_PARAMETERS,git "$@"andssh. Name them in the README speed-bump caveat, with setkernel/hq#209 as the real boundary.hooks/libwith its own test file.— Claude Lead
realRaghavGupta commentedon Oct 27, 2026
Row 2 of the second check is fixed in #44 at
045b3172. That covers2>&1 | sh,>&2 | shand|& 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
realRaghavGupta commentedon Oct 27, 2026
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-guardblocks:-Cvalue (git -C '' push). The empty value is dropped, andpushis then read as the directory. The old guard also gave 0. (orion 2)<<inside an arithmetic command,(( x << 2 )), is read as a heredoc whose delimiter is2. The lines up to a line that is exactly2are then dropped as data. The same goes forfor ((;;))and(( 1<<2 )). It is contrived. (lyra 2)For the README speed-bump caveat:
python3 -cnote. (orion 1)xargs sh -c,. /dev/stdin,source /dev/stdinorbusybox sh;bash <(echo …);evalof an escaped blank;eval "$(echo …)".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
realRaghavGupta commentedon Oct 27, 2026
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 withoutpython3would then refuse every Bash call, while POSIXshandawkare on every seat.Order of work:
git-guard.shintoplugins/setkernel/hooks/lib/scrub.awk, run withawk -f.tests/run.shrow withsk-push-guard's code: 2 for the pushes, 0 for the false refusals.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
lyra-setkernel commentedon Oct 27, 2026
Picked up. Working on branch
agent/45-git-guard-no-push-mode-after-44-escape-e. Automated: @lyra-setkernel's agent onlyra-dev.lyra-setkernel commentedon Oct 27, 2026
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 pushpast the guard.sk-push-guardand 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
tests/run.shwith 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 intoplugins/setkernel/hooks/lib/scrub.awkwith no behaviour change. Then I'll add the rows.Changed
agent/45-git-guard-no-push-mode-after-44-escape-eis still clean atfd67fca.$TMPDIR/h: a comparison script, a setup script and two empty test repos. These get removed when the task ends.bash tests/run.shon the unchanged tree:Found
tests/run.sh:622-625). busybox sh finds its own awk even withPATHemptied, so there's no awk to take away. I confirmed this by diffing the test lists from the two runs.push --forcewas blocked there. Any work on git-guard no-push mode after #44: escape-encoded feeders, fail-open fallback, contrived false refusals, speed #45 has to avoid that.original-awkand no C compiler.nawkhere is gawk, so only mawk, gawk and busybox awk can be checked. The macOS checks stay with Claude Lead's runs.Handing to a human. Automated: @lyra-setkernel's agent on
lyra-dev.realRaghavGupta commentedon Oct 27, 2026
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.
git-guard.shintoplugins/setkernel/hooks/lib/scrub.awk, and run it withawk -f, found relative to the hook's own path.scrub.awksaying what it reads and prints.git-guard.shworks from any working directory, so the path to the file must resolve.tests/run.shunchanged. The suite must pass with the same count under dash, bash and busybox sh, and with mawk and gawk.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
lyra-setkernel commentedon Oct 27, 2026
Picked up. Working on branch
agent/45-git-guard-no-push-mode-after-44-escape-e. Automated: @lyra-setkernel's agent onlyra-dev.refactor: move the no-push scrub awk program into hooks/lib/scrub.awk…