shelpaのパス制限・監査ミラーと廃止の理由
LLM向けMCPシェルshelpaのパス脱出とメモリ上限を対策しました。LLM導入で直面したツール選択・パイプ構文の問題と、復旧に使えた監査ミラーを記録します。
shelpaを廃止した理由
shelpaは、LLMエージェントの書き込みをteeに限定するMCP(Model Context Protocol)の仮想パイプラインシェルです。すべての書き込みを.shelpa/へミラーし、編集履歴を残しました。
パス脱出やメモリ上限の対策を実装しましたが、モデルに使用を定着させられず廃止しました。LLM導入では、制限を実装するだけでなく、実際のツール選択も確認する必要がありました。
teeへ書き込みを集約する
読み取りと書き込みの分離
write_fileやedit_fileを使う代わりに、UNIXパイプラインのteeだけで書き込む設計です。
rg "pattern" src/ | awk '{print $2}' | tee output.txt
rg、awk、sed、jq等は読み込みバッファとして使う- 最終的に
teeで書き出す — これが唯一の書き込み手段 teeの書き込みは自動的に.shelpa/にミラーコピーされ、編集履歴が残る
シェル知識を使わせる仮説
インターフェースをシェルに近づけ、モデルが学習済みのコマンド知識を使えると考えました。
tail、rg、awk、sed、teeに寄せたツール名を使いました。
| ツール名 | 役割 |
|---|---|
shelpa_pipe | パイプラインコマンドの実行 |
shelpa_tee | ファイル書き込み(tee 経由) |
shelpa_write | 直接書き込み(tee のラッパー) |
shelpa | ナビゲーション(cd, pwd) |
ヘルプ出力です。
shelpa-mcp (MCP stdio server)
Usage:
shelpa-mcp [--root <ROOT>] [--help]
Notes:
- This binary speaks MCP over stdio. It does not serve HTTP.
- Use your MCP client to call tools below.
- --root sets the workspace root directory for all tool calls.
- cwd defaults to the workspace root if not provided.
Tools:
- shelpa_pipe Execute a virtual safety pipeline
(tail rg awk sed tr jq wc tee fd ls sort head)
- shelpa_write Execute a virtual safety tee (auto guard, save override history)
Allowed pipeline commands: tail rg awk sed tr jq wc tee fd
Navigation commands (single-stage only): pwd cd <path> ls [path]
Pipes only. No redirects (> >>). No sed -i. No awk file output. Save via tee only.
CRITICAL: Never use standard file editing tools (such as write_file, replace, etc.)
- always use the specified tool exclusively.
shelpa_pipeからパイプライン実行を認識する想定でした。ヘルプのCRITICALでも、標準の編集ツールを使わずshelpaを使うよう指示しました。
セキュリティ設計
脆弱性1:cwd オプションによるパス脱出
tool_pipeのcwdはcanonicalizeしていましたが、ワークスペースルート内かを確認していませんでした。
// 修復後
let canonical = fs::canonicalize(&resolved)?;
if !canonical.starts_with(root.canonicalize()?) {
return Err(GuardViolation::new(
GuardReason::PathEscape,
"cwd must be within workspace root"
));
}
脆弱性2:.shelpa 内のパス脱出
shelpa_write_pathがtargetを直接joinしていたため、../で.shelpa外へ書き込めました。
// 修復後:正規化済みの実パスから相対パスを計算
pub fn shelpa_write_path(workspace_root: &Path, real_path: &Path) -> Result<PathBuf, GuardViolation> {
let real_canonical = fs::canonicalize(real_path)?;
let relative = real_canonical.strip_prefix(workspace_root.canonicalize()?)
.map_err(|_| GuardViolation::new(GuardReason::PathEscape, "Path is outside workspace"))?;
Ok(workspace_root.join(".shelpa").join(relative))
}
脆弱性3:tee のメモリバッファ無制限
teeが全出力をメモリへ保持し、大容量出力でOOMになりうる問題がありました。2段階の上限で対応しました。
- ハードリミット(1MB):超過分は切り捨て、
truncated=trueをメタデータに設定 - ソフトリミット(2KB、承認ゲート):超過時は
APPROVAL_REQUIREDエラーを返し、ユーザーの明示的な承認を要求
if out_data.len() > TEE_APPROVAL_BYTES && !confirm_oversize {
return Err(GuardViolation::new(
GuardReason::ApprovalRequired,
format!("tee would write {} bytes, exceeding the {}-byte threshold. \
Re-invoke with confirm_oversize=true to proceed.",
out_data.len(), TEE_APPROVAL_BYTES)
));
}
監査ミラーの設計
teeは実ファイルと同じ内容を.shelpa/へ残します。上書き時にはタイムスタンプ付きセパレータを挿入しました。
--- shelpa:overwrite ts=2026-02-25T13:17:30Z record_id=1772025450395572000 ---
(新しい内容)
運用で起きた問題
使用を定着させられなかった
モデルは「shelpa経由で書く」という指示を継続して守れませんでした。
起きた問題は次のとおりです。
- ツール選択の優先度が低い:LLM はデフォルトで
write_fileやedit_fileを使おうとする。システムプロンプトで shelpa の使用を強制しても、コンテキストが長くなると忘れる - パイプライン構文の構築ミス:
rg pattern | awk '{print $2}' | tee output.txtのようなパイプラインを正しく構築できないケースが頻発。クォートのエスケープで壊れることが多い - シェル偽装は通用しなかった:
shelpa_pipe、shelpa_teeというシェル風の命名で LLM を騙す狙いだったが、モデルは「これはシェルの一種」とは認識してくれなかった。事前学習で獲得した shell 知識を流用させるという目論見は、思ったより効果が小さかった
この実装で分かったこと
1. 指示だけではツール選択を揃えられない
今回のモデルは既存のツール使用パターンに引かれ、システムプロンプトだけでは独自インターフェースへ揃えられませんでした。
2. 名前をシェルに寄せても不十分
shelpa_pipe、shelpa_teeという名前でも、write_fileを選ぶ挙動は変わりませんでした。pipeへワンライナーを送る場合もありましたが、その内容を精査する負担が残りました。
3. 監査ミラーは復旧に使えた
.shelpa/の記録は事後監査に役立ちました。パターンマッチングで戻したいチェックポイントを探せるため、重要なファイルを誤って上書きした際の復旧に使えました。
