背景
#575 の着手前調査(2026-10-03、H-01)で見つかった見逃しです。develop にもとからあります。2026-10-03 の着手前調査(develop@66aa332 、#575 ・#568 の merge 後)で、範囲と直し方を確かめました。判定の関数を呼んで結果を確かめ、shell の挙動は scratchpad の中で echo を使って確かめただけです。
問題(背景)
Bash の字句解析は、コメント(語の頭の # から行末)と heredoc の本文を知りません。その中の '・" を引用の始まりと読むので、後ろのコマンドが検査されません。字句解析が None を返すと書き込み先は空になり、許可されます。行末に #' を足して数をそろえると Some が返り、やはり許可されます。同じ字句解析の問題が、秘密情報の検出と 2 段目の path の候補にもあります。
入力(⏎ は改行。sub/link は外を指す symlink)
今の判定
echo x # it's⏎tee sub/link/f、cat <<EOF⏎it's⏎EOF⏎tee /tmp/f
許可
echo x # it's⏎tee /tmp/f #'、… $'tee' /tmp/f #'、… cd sub && tee link/f #'
許可、bash:verify でも自動許可
echo x # it's⏎cat sub/link/secret #'
許可(外を読む)
echo x # it's⏎cat .env、cat .e\⏎nv
秘密情報の検出を素通り
cat <<EOF > sub/link/f⏎it's⏎EOF、cat <<EOF | tee sub/link/f⏎it's⏎EOF
許可(heredoc の行の書き込み先も消える)
tee sub/link/f⏎echo 'abc
許可(sh -c は後ろの行が構文エラーでも前の行を実行する)
逆に、コメントや本文の中の語で誤拒否もします(echo x # tee sub/link/f、cat <<EOF⏎tee /tmp/f⏎EOF、cd sub && tee f # CDPATH、echo x # cat .env の秘密情報の検出)。
影響(背景)
ワークスペースの外への書き込みと読み取り、秘密情報の参照の検出の素通り、bash:verify の自動許可が起きます。記録に残る agent の Bash コマンドでは heredoc は 0 件、コメントも 0 件でした(引用の中の # だけ)。
決めた設計(2026-10-03、2026-10-04 の merge 前レビューで 2・6・7・10 を改訂、同日の直した箇所の確認で 2・6 を再改訂、さらに 2・6 を三改訂、2 を四改訂、2 を五改訂)
コメントと heredoc の本文を落とす走査を、新しい葉の module に 1 つだけ作る。 結果は「整えた文字列」(Some)か「読めない」(None)。書き込み検査、cwd の候補、2 段目の path の候補、秘密情報の検出の 4 つの字句解析は、中身を変えずに、入口でこの結果を読む。引用の状態の機械を字句解析ごとに書き直さない。
走査の決まり (読み飛ばしは shell より短くなる向きにだけ誤る):
# は、語の頭(初め、空白・タブ・改行、;&|()<> の後)で、引用とバッククオートの外にあるときだけコメントにし、行末まで落とす。バッククオートの中の # はコメントにしない。\r・\f などは語の区切りにしない(bash の blank は空白とタブだけ)。
\⏎(行の継続)は落とし、その前の「語の頭」の状態を引き継ぐ。<<<(here-string)は heredoc にしない。
<< があって、コマンドの中に (( があれば None(算術のシフトと取り違えないため)。
区切りの語は引用を外して比べる。引用があれば「引用した区切り」。本文は次の引用外の改行から始まる。1 行に heredoc が複数あれば順に読む。終わりの区切りは行全体の完全一致で探す(<<- は先頭のタブを落としてから)。見つからなければ残り全部が本文。
次の場合は None(読めない)にする(2026-10-04 追加):区切りの語に \⏎ がある。区切りの "…" の中に、$・`・"・\ 以外の文字の前の \ がある。引用しない区切りの本文に \ で終わる行がある。引用しない区切りの本文に $( かバッククオートがある(本文の中で実行されるため。本文をコマンドとして読み直さない)。
本文を落とすのは、次をすべて満たすときだけにする(2026-10-04 四改訂。関数・alias の見分けを綴りで行うと a""lias・func\⏎tion などを見逃したため、同じコマンドの中に定義の余地がない形に限る):
コマンド全体(heredoc の本文を除き、コメントと \⏎ を落としたもの)が、コマンド 1 つだけで、;・|・&・(・)・{・}・$(・バッククオート・>(・<(・改行(本文の前後を除く)を含まない(redirect は可)。
heredoc は 1 つだけで、区切りの語を引用している(<<'EOF'・<<"EOF"・<<\EOF)。
引用を外した argv が次の形のどれかに完全に合う:cat(引数なし)、git commit または git tag(git の直後がその subcommand で、-F - を含み、-c・--stdin* を含まない)、gh の issue・pr の comment・create・edit で --body-file -。名前に引用・\・$・`・/ を含む形は合わない。
残した(実行される)本文の中にある heredoc の本文は、上の条件に合っても落とさない (2026-10-04 五改訂。外側で export した関数や PATH の書き換えで、内側の cat が本文を実行する形を見逃したため)。
それ以外の heredoc の本文は命令として残す。残す本文は、コメントを落とした読み方と落とさない読み方の両方で読み、両方の候補を検査する。どちらかが読めなければ None にする(shell 以外のコマンドの本文に shell のコメントの規則だけを当てると、Python の行内コメントの ' で後ろが隠れたため)。
heredoc の区切りや区切りの行に \r があれば None にする(区切りの比較で \r を外さない)。
書き込み検査 :整えた文字列が None、または整えた文字列でも shell_tokens が None なら、新しい operation(例:"unreadable shell text")で拒否する(正直な失敗)。sh -c は後ろの行が構文エラーでも前の行を実行するため、None を「何も走らない」とは見なさない。cwd の候補の拒否からもこの operation を除く。
ANSI-C 引用の走査 ([security][tools] Bash の書き込み検査の字句解析が ANSI-C 引用 $'…' を扱わず、program や option を見逃してワークスペース外へ書き込める(#566 の再確認で発見) #575 )は、生の文字列と整えた文字列の両方 に掛ける(片方だけだと、拒否の理由が変わるか、# it's⏎$'tee' #' を見逃す)。
cwd の候補 ([security][tools] Bash の書き込み検査が同じコマンドの cd・pushd の後の cwd を追わず、symlink を通ってワークスペース外へ書き込める(#509 の調査で発見) #568 )は、整えた文字列が None なら「決められない」とする。CDPATH・DIRSTACK・ループの判定は整えた文字列の token から読む(コメントの中の語で誤拒否しない)。
2 段目の path の候補 は、整えた文字列を読む(本文を落とす条件は設計 2 で絞る)。秘密情報の検出 は、整えた文字列の token に加えて、heredoc の本文(落とした本文も残した本文も)を \⏎ を外したうえで空白と引用の記号で区切った語ごとに、今の秘密名の判定に掛ける(2026-10-04 三改訂。本文の引用・行の継続で .env が隠れたため。厳しい側)。
bash:verify の自動許可 :整えた文字列が生の文字列と違う(コメント・heredoc・\⏎ がある)コマンドは、自動許可しない。verify の分類は改行を置き換えた文字列を見るため、読み飛ばしを入れるとコメントが次の行まで飲み込み、echo x # cd sub⏎tee link/f が自動許可に変わってしまうため。verify の分類そのものは変えない。あわせて、生の文字列でも書き込みが認識されないこと(has_recognized_mutation)を条件に足す(2026-10-04 追加。垂直タブや Unicode の空白の後の #x;rm -rf src を、verify の正規化がコメントにして自動許可していたため)。
ShellToken と shell_tokens の中身、前置きの Resolution、event の名前と schema は変えない(operation の値が 1 つ増えるだけ)。
既存の単体テストのうち tee 'unterminated の期待を、「対象なし」から「拒否」に変える(厳しくする向き)。
誤拒否として受け入れる形:バッククオートの中のコメント(echo `echo x # it's`)、$((1<<2)) と heredoc の組み合わせ、sh <<'EOF'⏎echo it's⏎EOF。2026-10-04 追加:データとして読む一覧にないコマンドへの、読めない本文の heredoc(python3 <<'EOF'⏎# it's⏎EOF など)、引用しない本文に $( を含む heredoc(cat <<EOF > notes.md⏎v $(git rev-parse HEAD)⏎EOF)、本文の中の .env などの語の秘密情報の検出(cat <<'EOF'⏎cat .env⏎EOF)。2026-10-04 四改訂で追加:区切りを引用していない heredoc や、ほかのコマンドとつないだ heredoc で、本文が読めない形(cat <<EOF > out.txt⏎it's⏎EOF、git add x && git commit -F - <<'EOF'⏎it's⏎EOF)。
範囲外:バッククオートのコマンド置換(echo `tee sub/link/f`。別の Issue)、sh -c '…'・eval(#502 )。python3・node・perl などのコードが文字列の連結や計算で組み立てる path(open("sub/""link/f") など。#502 で扱う。shell の字句解析では決めきれないため)。コマンドのどこかに $ があると、2 段目の path 検査が静的な相対パスを候補にせず、symlink 経由の外の読み取りを見逃す形(#582 。#576 の前から develop にある、path_tokens.rs の既存の問題。H-07 で発見)。
対象ファイル
src/tools/shell_lexical.rs — 新規。コメントと heredoc の本文を落とす走査と、単体テスト
src/tools/mod.rs — module の登録(1 行)
src/tools/bash_write_guard.rs — 書き込み検査の配線、新しい operation、ANSI-C 走査を両方に掛ける、既存の単体テストの期待 1 件
src/tools/bash_write_guard/working_directory.rs — 整えた文字列が読めないときの扱い(数行)
src/tools/bash/path_tokens.rs — 整えた文字列を読む(数行)
src/tools/sensitive_path.rs — 整えた文字列を読む(数行)
src/tools/allow_policy.rs — bash:verify の自動許可の条件(数行)
tests/issue576_bash_comment_heredoc.rs — 新規。path_confinement_rejection などを呼ぶ結合テスト
受け入れ条件
テストの設計(参考)
拒否する形(/tmp/f と、fixture の sub/link → 外):
問題の表の形すべて(読み取りと秘密情報は判定の関数で確かめるだけで、実際には読まない)
echo x #"⏎tee sub/link/f、# it's⏎tee sub/link/f #'、echo x;# it's⏎…、&&#・|#・(#…⏎)、$(echo x # it's⏎) ; tee …
区切りの 6 種類(EOF・-EOF・'EOF'・"EOF"・\EOF・ EOF)それぞれに #' を足した形、<<A <<B の形、区切りに似た行(EOF ・ EOF)の後の tee …、echo $(cat <<EOF⏎it's⏎EOF⏎) ; tee …、cat <<<"it's"⏎tee …
tee 'x、cat <<EOF⏎$(tee sub/link/f)⏎EOF、sh <<EOF⏎tee /tmp/f⏎EOF、bash -s <<EOF⏎tee sub/link/f⏎EOF、echo $((1<<2))⏎tee sub/link/f⏎2))
許可のままの形:
cat <<'EOF' > notes.md⏎it's⏎EOF、cat > notes.md <<'EOF'⏎it's "quoted⏎EOF、git commit -F - <<'EOF'⏎Fix it's bug⏎EOF
(2026-10-04 四改訂で誤拒否に変わる形:cat <<EOF > out.txt⏎it's⏎EOF(区切りを引用していない)、git add x && git commit -F - <<'EOF'⏎it's⏎EOF(コマンドが 2 つ)。設計 10 に含める)
cat <<EOF⏎tee /tmp/f⏎EOF、cat <<'EOF'⏎$(tee sub/link/f)⏎EOF、python3 - <<'EOF'⏎print("it's")⏎EOF
cargo test # don't⏎cargo build、echo x # tee sub/link/f、cd sub && tee f # CDPATH、echo a#b > out.txt、echo $# ${#x} ${x#y} > out.txt
テストの関数名には、結合テストは comment_heredoc、単体テストは shell_lexical を含めます(変異テストの名前のフィルタで、両方が走るようにするため)。
回帰で通す既存のテスト(参考)
tests/issue568_bash_cwd_write_targets.rs
tests/issue575_bash_ansi_c_quoting.rs
tests/issue566_bash_command_prefix_write_targets.rs
tests/issue567_bash_glob_write_targets.rs
tests/issue428_bash_path_tokens.rs
tests/issue501_sensitive_paths.rs(秘密情報の検出)
tests/bash_workspace_confinement.rs
作業の場所(参考)
ビルドとテストは CARGO_TARGET_DIR=/Volumes/SSD_NX/tmp/issue-576-20261003-01/target-576 と CARGO_INCREMENTAL=0 で行います。
変異テストのコマンド(--lib は付けない。付けると結合テストが走らない):
cargo mutants -f src/tools/shell_lexical.rs --jobs 2 --timeout 120 -o /Volumes/SSD_NX/tmp/issue-576-20261003-01/mutants -- shell_lexical comment_heredoc
ベースラインで単体テストと結合テストの両方が走ることを確かめます。打ち切った後は、ソースを touch してビルドし直します。
着手前調査の report:ローカルの workspace/tmp/1003/issue-576/results/H-01/report.md。probe と試作の patch:/Volumes/SSD_NX/tmp/issue-576-20261003-01/probe、proto-b.patch。
関連(参考)
背景
#575 の着手前調査(2026-10-03、H-01)で見つかった見逃しです。develop にもとからあります。2026-10-03 の着手前調査(develop@66aa332、#575・#568 の merge 後)で、範囲と直し方を確かめました。判定の関数を呼んで結果を確かめ、shell の挙動は scratchpad の中で echo を使って確かめただけです。
問題(背景)
Bash の字句解析は、コメント(語の頭の
#から行末)と heredoc の本文を知りません。その中の'・"を引用の始まりと読むので、後ろのコマンドが検査されません。字句解析がNoneを返すと書き込み先は空になり、許可されます。行末に#'を足して数をそろえるとSomeが返り、やはり許可されます。同じ字句解析の問題が、秘密情報の検出と 2 段目の path の候補にもあります。echo x # it's⏎tee sub/link/f、cat <<EOF⏎it's⏎EOF⏎tee /tmp/fecho x # it's⏎tee /tmp/f #'、… $'tee' /tmp/f #'、… cd sub && tee link/f #'echo x # it's⏎cat sub/link/secret #'echo x # it's⏎cat .env、cat .e\⏎nvcat <<EOF > sub/link/f⏎it's⏎EOF、cat <<EOF | tee sub/link/f⏎it's⏎EOFtee sub/link/f⏎echo 'abc逆に、コメントや本文の中の語で誤拒否もします(
echo x # tee sub/link/f、cat <<EOF⏎tee /tmp/f⏎EOF、cd sub && tee f # CDPATH、echo x # cat .envの秘密情報の検出)。影響(背景)
ワークスペースの外への書き込みと読み取り、秘密情報の参照の検出の素通り、bash:verify の自動許可が起きます。記録に残る agent の Bash コマンドでは heredoc は 0 件、コメントも 0 件でした(引用の中の
#だけ)。決めた設計(2026-10-03、2026-10-04 の merge 前レビューで 2・6・7・10 を改訂、同日の直した箇所の確認で 2・6 を再改訂、さらに 2・6 を三改訂、2 を四改訂、2 を五改訂)
Some)か「読めない」(None)。書き込み検査、cwd の候補、2 段目の path の候補、秘密情報の検出の 4 つの字句解析は、中身を変えずに、入口でこの結果を読む。引用の状態の機械を字句解析ごとに書き直さない。#は、語の頭(初め、空白・タブ・改行、;&|()<>の後)で、引用とバッククオートの外にあるときだけコメントにし、行末まで落とす。バッククオートの中の#はコメントにしない。\r・\fなどは語の区切りにしない(bash の blank は空白とタブだけ)。\⏎(行の継続)は落とし、その前の「語の頭」の状態を引き継ぐ。<<<(here-string)は heredoc にしない。<<があって、コマンドの中に((があればNone(算術のシフトと取り違えないため)。<<-は先頭のタブを落としてから)。見つからなければ残り全部が本文。None(読めない)にする(2026-10-04 追加):区切りの語に\⏎がある。区切りの"…"の中に、$・`・"・\以外の文字の前の\がある。引用しない区切りの本文に\で終わる行がある。引用しない区切りの本文に$(かバッククオートがある(本文の中で実行されるため。本文をコマンドとして読み直さない)。a""lias・func\⏎tionなどを見逃したため、同じコマンドの中に定義の余地がない形に限る):\⏎を落としたもの)が、コマンド 1 つだけで、;・|・&・(・)・{・}・$(・バッククオート・>(・<(・改行(本文の前後を除く)を含まない(redirect は可)。<<'EOF'・<<"EOF"・<<\EOF)。cat(引数なし)、git commitまたはgit tag(gitの直後がその subcommand で、-F -を含み、-c・--stdin*を含まない)、ghのissue・prのcomment・create・editで--body-file -。名前に引用・\・$・`・/を含む形は合わない。catが本文を実行する形を見逃したため)。Noneにする(shell 以外のコマンドの本文に shell のコメントの規則だけを当てると、Python の行内コメントの'で後ろが隠れたため)。\rがあればNoneにする(区切りの比較で\rを外さない)。None、または整えた文字列でも shell_tokens がNoneなら、新しい operation(例:"unreadable shell text")で拒否する(正直な失敗)。sh -c は後ろの行が構文エラーでも前の行を実行するため、Noneを「何も走らない」とは見なさない。cwd の候補の拒否からもこの operation を除く。# it's⏎$'tee' #'を見逃す)。Noneなら「決められない」とする。CDPATH・DIRSTACK・ループの判定は整えた文字列の token から読む(コメントの中の語で誤拒否しない)。\⏎を外したうえで空白と引用の記号で区切った語ごとに、今の秘密名の判定に掛ける(2026-10-04 三改訂。本文の引用・行の継続で.envが隠れたため。厳しい側)。\⏎がある)コマンドは、自動許可しない。verify の分類は改行を置き換えた文字列を見るため、読み飛ばしを入れるとコメントが次の行まで飲み込み、echo x # cd sub⏎tee link/fが自動許可に変わってしまうため。verify の分類そのものは変えない。あわせて、生の文字列でも書き込みが認識されないこと(has_recognized_mutation)を条件に足す(2026-10-04 追加。垂直タブや Unicode の空白の後の#x;rm -rf srcを、verify の正規化がコメントにして自動許可していたため)。tee 'unterminatedの期待を、「対象なし」から「拒否」に変える(厳しくする向き)。echo `echo x # it's`)、$((1<<2))と heredoc の組み合わせ、sh <<'EOF'⏎echo it's⏎EOF。2026-10-04 追加:データとして読む一覧にないコマンドへの、読めない本文の heredoc(python3 <<'EOF'⏎# it's⏎EOFなど)、引用しない本文に$(を含む heredoc(cat <<EOF > notes.md⏎v $(git rev-parse HEAD)⏎EOF)、本文の中の.envなどの語の秘密情報の検出(cat <<'EOF'⏎cat .env⏎EOF)。2026-10-04 四改訂で追加:区切りを引用していない heredoc や、ほかのコマンドとつないだ heredoc で、本文が読めない形(cat <<EOF > out.txt⏎it's⏎EOF、git add x && git commit -F - <<'EOF'⏎it's⏎EOF)。範囲外:バッククオートのコマンド置換(
echo `tee sub/link/f`。別の Issue)、sh -c '…'・eval(#502)。python3・node・perlなどのコードが文字列の連結や計算で組み立てる path(open("sub/""link/f")など。#502 で扱う。shell の字句解析では決めきれないため)。コマンドのどこかに$があると、2 段目の path 検査が静的な相対パスを候補にせず、symlink 経由の外の読み取りを見逃す形(#582。#576 の前から develop にある、path_tokens.rsの既存の問題。H-07 で発見)。対象ファイル
src/tools/shell_lexical.rs— 新規。コメントと heredoc の本文を落とす走査と、単体テストsrc/tools/mod.rs— module の登録(1 行)src/tools/bash_write_guard.rs— 書き込み検査の配線、新しい operation、ANSI-C 走査を両方に掛ける、既存の単体テストの期待 1 件src/tools/bash_write_guard/working_directory.rs— 整えた文字列が読めないときの扱い(数行)src/tools/bash/path_tokens.rs— 整えた文字列を読む(数行)src/tools/sensitive_path.rs— 整えた文字列を読む(数行)src/tools/allow_policy.rs— bash:verify の自動許可の条件(数行)tests/issue576_bash_comment_heredoc.rs— 新規。path_confinement_rejection などを呼ぶ結合テスト受け入れ条件
cat(){ sh; }⏎export -f cat⏎sh <<'OUTER'⏎cat <<'INNER'⏎tee sub/li"nk/f"⏎INNER⏎OUTERなど。PATH を書き換えた形を含む)が拒否される(保護 path は P=1、秘密は S=1)a""lias cat=sh⏎cat <<'EOF'⏎tee sub/link/f⏎EOF、func\⏎tion cat { sh; }⏎cat <<'EOF'⏎…など)の形が拒否されるgit・ghの argv の引用、Python の本文のコメント、本文の中の秘密名)の形が拒否される(または秘密情報が検出される)git・gh・openssl、区切りの\r、残す本文のコメント、本文に'があるときの秘密情報の見逃し)の形が拒否される(または秘密情報が検出される)\r・\f、\⏎の後の語の頭、bash:verify の自動許可)の形が拒否される(または自動許可されない)echo x # it's⏎cat .env・cat .e\⏎nvが検出され、echo x # cat .envは検出されない(cat <<'EOF'⏎cat .env⏎EOFは 2026-10-04 の設計 6 の改訂で検出する側に変えた)echo x # cd sub⏎tee link/f・cargo test # it's⏎rm -rf src #'・cargo test␋#x;rm -rf src(␋ は垂直タブ)が自動許可されず、cargo test・cd frontend && npm testは自動許可のままテストの設計(参考)
拒否する形(/tmp/f と、fixture の sub/link → 外):
echo x #"⏎tee sub/link/f、# it's⏎tee sub/link/f #'、echo x;# it's⏎…、&&#・|#・(#…⏎)、$(echo x # it's⏎) ; tee …EOF・-EOF・'EOF'・"EOF"・\EOF・EOF)それぞれに#'を足した形、<<A <<Bの形、区切りに似た行(EOF・EOF)の後のtee …、echo $(cat <<EOF⏎it's⏎EOF⏎) ; tee …、cat <<<"it's"⏎tee …tee 'x、cat <<EOF⏎$(tee sub/link/f)⏎EOF、sh <<EOF⏎tee /tmp/f⏎EOF、bash -s <<EOF⏎tee sub/link/f⏎EOF、echo $((1<<2))⏎tee sub/link/f⏎2))許可のままの形:
cat <<'EOF' > notes.md⏎it's⏎EOF、cat > notes.md <<'EOF'⏎it's "quoted⏎EOF、git commit -F - <<'EOF'⏎Fix it's bug⏎EOFcat <<EOF > out.txt⏎it's⏎EOF(区切りを引用していない)、git add x && git commit -F - <<'EOF'⏎it's⏎EOF(コマンドが 2 つ)。設計 10 に含める)cat <<EOF⏎tee /tmp/f⏎EOF、cat <<'EOF'⏎$(tee sub/link/f)⏎EOF、python3 - <<'EOF'⏎print("it's")⏎EOFcargo test # don't⏎cargo build、echo x # tee sub/link/f、cd sub && tee f # CDPATH、echo a#b > out.txt、echo $# ${#x} ${x#y} > out.txtテストの関数名には、結合テストは comment_heredoc、単体テストは shell_lexical を含めます(変異テストの名前のフィルタで、両方が走るようにするため)。
回帰で通す既存のテスト(参考)
作業の場所(参考)
cargo mutants -f src/tools/shell_lexical.rs --jobs 2 --timeout 120 -o /Volumes/SSD_NX/tmp/issue-576-20261003-01/mutants -- shell_lexical comment_heredoc
ベースラインで単体テストと結合テストの両方が走ることを確かめます。打ち切った後は、ソースを touch してビルドし直します。
関連(参考)
根拠(develop@66aa332)