Skip to content

Approval gate: read command substitutions (incl. unquoted heredoc bodies) and eval - #299

Merged
yuanhao merged 1 commit into
mainfrom
fix/gate-substitutions
Oct 11, 2026
Merged

yuanhao merged 1 commit into
mainfrom
fix/gate-substitutions

Conversation

@yuanhao

@yuanhao yuanhao commented Oct 11, 2026

Copy link
Copy Markdown
Collaborator

From review of #298.

[P1] Unquoted heredocs could run commands without approval. The shell expands an unquoted heredoc body, so $(rm -f owner.txt) or a backquoted command inside it really runs. The gate skipped the body as data.

The same gap existed anywhere a substitution runs:

  • echo $(rm -f owner.txt)
  • echo "$(rm -f owner.txt)"
  • backquotes
  • diff <(rm -f owner.txt) x and >( … )
  • an assignment's value: x=$(git reset --hard)
  • a substitution inside $(( … ))
  • eval "rm -f owner.txt"

Fix:

  • The tokenizer collects the commands each simple command's substitutions run. It looks inside double quotes and unquoted heredoc bodies; a quoted or escaped delimiter keeps the body literal, as in the shell.
  • The checker reads them like any command, in that command's directory, so cd sub && echo $(rm -f only.txt) is checked in sub.
  • eval of literal text is read the same way; eval of unknown text asks.
  • Everyday forms stay free: $(date), $(git rev-parse HEAD) > rev.txt, $(( 1 + 2 )), and a heredoc with a quoted delimiter.

Tests: 22 new cases (14 that must ask, 8 that must stay free). On the gate on main they fail on the reported heredoc; with the fix, all pass. The pi dialog test and coding_agent's CI check also pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG

From review of #298: an unquoted heredoc body is expanded by the shell, so
`cat <<EOF` / `$(rm -f owner.txt)` / `EOF` ran the deletion without
approval. The same held anywhere a substitution runs: `echo $(rm …)`,
`"$(rm …)"`, backquotes, `<( … )` / `>( … )`, an assignment's value,
inside `$(( … ))`, and `eval`.

The tokenizer now collects what each command's substitutions run (also
inside double quotes and unquoted heredoc bodies; a quoted or escaped
delimiter keeps the body literal) and the checker reads it like any
command, in that command's directory. `eval` of literal text is read the
same way; of unknown text it asks. Everyday forms stay free: `$(date)`,
`$(git rev-parse HEAD) > rev.txt`, `$(( 1 + 2 ))`.

Tests: 22 cases; on the previous gate they fail on the reported heredoc.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG
@yuanhao
yuanhao merged commit ee9dd9b into main Oct 11, 2026
17 checks passed
@yuanhao
yuanhao deleted the fix/gate-substitutions branch October 11, 2026 15:06
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.

1 participant