Skip to content

Guards: git-guard fails open on a jq parse error; size and SHELLOPTS edges that onFailure:block turns into blocks #42

Open

Description

@realRaghavGupta

Four hardening items for the two Bash guards (plugins/setkernel/hooks/git-guard.sh, d1-guard.sh). Claude Lead's adversarial check of #41 (onFailure: "block") found them. #41 makes a timeout or an exit other than 0 or 2 block the Bash call. None of these blocks #41: each needs a setup no seat has today. All four predate it. Each was reproduced on Raghav's Mac (bash 3.2 as /bin/sh, BSD awk, jq 1.7.1) against the scripts at #41's head 0ed3d205 (same as main), and then checked by a second agent.

1. git-guard fails open when jq can't parse the hook JSON

  • A lone surrogate escape anywhere in the input makes jq -r '.tool_input.command // empty' fail (Invalid \uXXXX\uXXXX surrogate pair escape, rc 5). The guard discards jq's stderr, gets an empty cmd and exits 0, so a blocked command in the same input goes through unchecked.
  • Repro: write a hook JSON whose command is echo \ud800 x; followed by a force push, with the \ud800 as a literal JSON escape. Then run sh plugins/setkernel/hooks/git-guard.sh < that.json: rc 0. Without the \ud800 it is rc 2.
  • It is rare, because Claude Code only writes a lone surrogate when the command text holds one. But it is the one input found that turns a block into a pass. The VMs run jq 1.8.1, which wasn't tested.
  • When jq exits non-zero on non-empty input, use the sed fallback (as when jq is missing) instead of an empty command. Keep exit 0 for empty, non-JSON, {} and non-Bash input, as tests/run.sh requires.

2. git-guard slows down with the square of the command size under bash 3.2 (Macs)

  • check_cmd keeps the whole command in $1 while it calls check_words once per segment. bash 3.2 copies the caller's positional parameters on every function call, so a big command costs size × segments.
  • Measured on the Mac's sh, dense shell text: 150 KB took 1.2 s, 500 KB 7.9 s, 600 KB 10.7 s (past the 10 s timeout) and 1 MB 27.7 s. dash and Linux bash 5.2 stay linear (1 MB in 1.4 s).
  • Real calls are far smaller. The largest Bash call among 40,917 in this Mac's transcripts was 68 KB, which takes 0.84 s worst case. So this is headroom, not a live problem.
  • Add set -- in check_cmd once segs is computed. On the scratch copy, this brought 1 MB down to 2.1–4.1 s.

3. d1-guard's comment scan is quadratic in BSD awk (Macs)

  • The awk loop at lines 68-80 calls substr per character on one growing string. A command with a remote wrangler d1 execute needs about 730 KB idle, or about 500 KB with all cores busy, to pass 10 s. 256 KB took 1.45 s.
  • Scan per line ($0) and carry the quote state across lines, or use index()/match().

4. git-guard exits 1 under an exported SHELLOPTS=errexit when /bin/sh is bash

  • Commands: git reset --soft, git reset --mixed, git reset HEAD <file> and git clean --dry-run.
  • Cause: for a; do abbrev "$a" --hard && block …; done leaves status 1, and shift && check_git "$@" is the last command of an AND list.
  • Only a launcher that exports SHELLOPTS triggers it; dash ignores it.
  • End those branches with return 0 (or || :), or unset SHELLOPTS at the top.

Also checked, fine

  • Missing or unreadable script:
  • 64 inputs under each of the Mac's sh, dash and bash --posix, plus 69 case files in the Linux runner image. These include aliases with chained, ! and self-referencing forms, unicode, NUL, heredocs, a missing -C dir, a deleted cwd, and no jq. Every one exited 0 or 2 with empty stdout, in 0.01–0.04 s.
  • On all four VMs (installed build 75f63c04c98f, byte-identical guards): every sample exits 0, or 2 for the force push, in 11–36 ms. Only d1-guard writes stdout, its 297-byte deny JSON. Claude Code is 2.1.296–2.1.301.

Not released: the v4c freeze (hq#368) holds one-off work. Claude Lead or Raghav adds agent-ready. Merge #41 first.

Refs #41, #36, setkernel/hq#332.

— Claude Lead

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: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