Skip to content

fix(readonly-bash): anchor git verbs to subcommand position + wrapper/indirection handling (#577)#697

Merged
griffinwork40 merged 2 commits into
mainfrom
afk/readonly-bash-577
Jul 24, 2026
Merged

fix(readonly-bash): anchor git verbs to subcommand position + wrapper/indirection handling (#577)#697
griffinwork40 merged 2 commits into
mainfrom
afk/readonly-bash-577

Conversation

@griffinwork40

Copy link
Copy Markdown
Owner

Closes #577.

Problem

classifyBashCommand matched git/curl verb regexes against the whole command string. A string match cannot tell a verb in command position from the same word in an argument, causing both defects #577 tracks:

  • Over-restriction (Tier 2): a bare pickaxe/grep/help arg that collides with a git verb was blocked — git log -S push, git log -G merge, git log --grep revert, git help push, git push --help. This hurt git-investigator, whose core job is git-history search.
  • Under-restriction (Tier 3): mutations reached via a wrapper (env/xargs/command/sudo/busybox), a shell variable (g=git; $g push), a pipe-to-interpreter (| python), or a quoted flag value (curl -X "POST") slipped through.

Fix

Replace the six anywhere-in-segment git-verb regexes with a small command-line tokenizer (Pass 1c) that:

  • splits pipeline/chaining segments (; | & ( )), quote-aware;
  • resolves one level of VAR=val; $VAR indirection (shared across segments);
  • unwraps no-op wrappers (env/xargs/command/sudo/nice/time/timeout/nohup/stdbuf/busybox/eval);
  • recurses into $(…)/backtick substitutions;

…then judges git by subcommand position (gitSegmentReason) and curl by parsed argv (curlSegmentMutating, catches the quoted -X "POST"). Also widens PIPE_TO_SHELL (more shells + piped interpreters python/perl/…) and INTERPRETER_WRITE_API (os.system/os.popen/subprocess./Deno.*).

git config stays regex-matched — its mutation signal (a quoted <value>) must be read on the raw string.

Conservative-by-construction (security)

Positional anchoring is more precise but was initially less conservative than the old blunt regex — an adversarial differential caught that it regressed ~12 git-mutation obfuscations the old code caught (eval git push, { git push; }, for…do git push, env g=git $g push, \git push, timeout -k 5 10 git push). A hiddenGitMutation backstop (+ eval/env/-k handling) restores them, so the classifier is strictly ≥ the old regex for the cage. A broad differential over ~55 realistic commands shows the only behavioral deltas vs. the pre-change classifier are the intended Tier-2 loosenings + tighter curl/indirection blocking — no new over-blocks on recon, no new bypasses.

Documented residual (asserted in a test)

$(echo git) push — a command substitution producing the command name — is not statically resolvable without executing it, so (like sh -c "git push") it's out of scope per the module's stated best-effort recon cage, not hostile-process sandbox threat model. Layer-1 write_file/edit_file strip remains the real boundary.

Acceptance criteria

  • git log -S push / --grep push / git help push read-only; git push / git -c x=y commit mutating.
  • find | xargs rm, env rm -rf, g=git; $g push, | python, os.system(...) mutating.
  • curl -X "POST" https://x mutating; grep -rn "curl -X POST" . read-only.
  • Existing 185 tests pass; new coverage per case.

Tests

  • readonly-bash.test.ts: 257 pass (185 existing + 72 new — acceptance cases, obfuscation backstop, asserted residual).
  • Consumers green: dispatcher (104), plan-mode-gate (16), nesting (28), agents/parser (13), plan-mode-addendum (12).
  • tsc --noEmit (strict): clean.
  • classifyBashCommand signature unchanged — zero API-compat impact.

🤖 Implemented + adversarially verified via agent-afk.

… wrapper/indirection (#577)

The classifier matched git/curl verb regexes against the whole command
string, which cannot distinguish a verb in COMMAND position from the same
word in an ARGUMENT. That over-blocked recon (`git log -S push`,
`git help push`) and under-blocked mutations reached via a wrapper
(env/xargs/command/sudo/busybox/eval), a shell variable (`g=git; $g push`),
or a quoted flag value (`curl -X "POST"`).

Replace the six anywhere-in-segment git-verb regexes with a small
command-line tokenizer (Pass 1c) that splits pipeline/chaining segments,
resolves one level of `VAR=val;$VAR` indirection, unwraps no-op wrappers,
and recurses into `$(…)`/backtick substitutions, judging the verb in
subcommand position. A `hiddenGitMutation` backstop keeps obfuscated
mutations (`eval git push`, `{ git push; }`, `\git push`) blocked so the
change is strictly MORE conservative than the old regex for the security
cage, never less. Also widen PIPE_TO_SHELL (more shells + piped
interpreters) and INTERPRETER_WRITE_API (os.system/os.popen/subprocess./
Deno.*), and add tokenizer-based curl `-X <method>` detection.

git config stays regex-matched (its quoted value must be read raw).

Tests: 185 existing + 72 new (#577 acceptance cases + obfuscation backstop
+ one asserted residual); consumers (dispatcher/plan-mode-gate/nesting/
parser/plan-mode-addendum) green; tsc --noEmit clean. Adversarially
verified against the pre-change classifier: the only behavioral deltas are
the intended over-block fixes + tighter curl/indirection blocking. One
documented residual: $(…)-in-command-position obfuscation, out of the
module's best-effort recon-cage threat model.
@vercel

vercel Bot commented Jul 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agent-afk-docs Ready Ready Preview, Comment Jul 24, 2026 9:19pm

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2d6a5899fc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agent/tools/readonly-bash.ts Outdated
Comment on lines +499 to +503
while (idx < tokens.length && (/^[A-Za-z_]\w*=/.test(tokens[idx]!) || tokens[idx]!.startsWith('-'))) {
const am = /^([A-Za-z_]\w*)=(.*)$/.exec(tokens[idx]!);
if (am !== null) vars.set(am[1]!, am[2]!);
idx++;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Consume env option operands before unwrapping

When env is used with an option that takes a separate operand, this loop skips only the flag and then treats that operand as the wrapped command. I checked env --help: -u, --unset=NAME and -C, --chdir=DIR are options before [COMMAND [ARG]...]; therefore env -u FOO git stash executes git stash, but resolveSegment returns command FOO, and because git stash was removed from STRIPPED_RULES while the backstop only covers always-mutating verbs, the classifier now allows this mutating command. Consume env option operands before deciding where the command starts.

Useful? React with 👍 / 👎.

Comment on lines +505 to +509
while (idx < tokens.length && tokens[idx]!.startsWith('-')) {
const f = tokens[idx]!;
idx++;
if (/^-[In]$/.test(f) && idx < tokens.length) idx++; // -I {} / -n N consume an arg
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Consume xargs long-option operands before unwrapping

For xargs options whose value is in the next token, this parser stops at the value and never unwraps the real command. I checked xargs --help: usage is [OPTION]... COMMAND and -n, --max-args=MAX-ARGS consumes an argument; find . | xargs --max-args 1 git tag -d v1 runs the mutating git tag -d, but the classifier treats 1 as the command and allows it because conditional git tag/stash/branch mutations no longer have regex backstops. Consume operands for long/short xargs options like --max-args, --max-lines, and --max-procs before returning.

Useful? React with 👍 / 👎.

…itional git mutations (#577)

Addresses Codex review on #697 (both P2):

- env `-u NAME` / `-C DIR` (+ `--unset`/`--chdir`/`-S`) and xargs
  `--max-args N` / `-n N` / `-L N` / `--max-procs N` / `-I R` (+ long forms)
  take a SEPARATE operand. The unwrap loops now consume it, so the operand is
  no longer mistaken for the wrapped command — `env -u FOO git stash` and
  `xargs --max-args 1 git tag -d` resolve `git` to command position.

- The hidden-git backstop now re-runs the full subcommand classifier
  (gitSegmentReason) on the tokens after any bare `git`, so CONDITIONAL
  mutations (stash / tag -d / branch -D / remote add / worktree remove)
  reached via a wrapper or compound form are caught too — not just the
  always-mutating verbs. Defense-in-depth against unenumerated wrapper options.

Tests: +13 (env/xargs operand + conditional-mutation cases); 270 classifier +
dispatcher/plan-mode-gate/nesting consumers green; tsc --noEmit clean.
@griffinwork40

Copy link
Copy Markdown
Owner Author

Thanks @codex — both P2 findings were valid and are fixed in 28938e1:

1. env option operandsenv -u NAME / -C DIR (+ --unset/--chdir/-S) now consume their separate operand in the unwrap loop, so the operand is no longer mistaken for the wrapped command. env -u FOO git stash resolves git to command position and blocks.

2. xargs option operands--max-args/-n / --max-lines/-L / --max-procs/-P / -I / -s / -d / -a / -E (short + long) now consume their operand too. find . | xargs --max-args 1 git tag -d v1 blocks.

Plus a defense-in-depth fix for the root gap you identified — the hidden-git backstop now re-runs the full subcommand classifier on tokens after any bare git, so conditional mutations (stash / tag -d / branch -D / remote add / worktree remove) reached via a wrapper or compound form are caught, not just always-mutating verbs.

+13 regression tests (270 classifier + consumers green, tsc clean).

@chatgpt-codex-connector

Copy link
Copy Markdown

Done

Testing

  • pnpm vitest run src/agent/tools/readonly-bash.test.ts
  • pnpm tsc --noEmit

View task →

@griffinwork40
griffinwork40 merged commit b9973e7 into main Jul 24, 2026
9 of 10 checks passed
@griffinwork40
griffinwork40 deleted the afk/readonly-bash-577 branch July 24, 2026 21:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

readonly-bash: subcommand anchoring + wrapper/indirection handling (follow-up to #574)

1 participant