Claude Media
DirectoryAddedフックで/add-dir追加後のディレクトリを監査して取り消す

DirectoryAddedフックで/add-dir追加後のディレクトリを監査して取り消す

/add-dirでディレクトリが追加された直後に発火するDirectoryAddedフック。追加そのものはブロックできない仕様と、取り消しが届く範囲・届かない範囲を解説します。

DirectoryAddedフックとは

DirectoryAddedは、セッションの途中で/add-dirコマンドを実行してディレクトリを追加した直後に発火するフックです。SDKクライアントがregister_repo_rootコントロールリクエストでディレクトリを登録した直後にも発火します。新しく増えたリポジトリの依存関係を入れておく、といった準備処理が公式の想定する使い方です。

このイベントが追加されたのはv2.1.219です。同じリリースでOpus 5が既定のOpusモデルになるなど大きな変更が並んだ回の一部です。セッション途中でディレクトリを広げる操作に、初めて監査の手が届くようになりました。/permissionsコマンドや許可ルールの運用と並べて使います。

Claude Codeはこのフックの完了を待ちません。追加はその場で終わり、フックは既定600秒のタイムアウトでバックグラウンドを走ります。サンドボックスと権限の状態はフック起動前に更新済みなので、サンドボックス化されたツールにも新しいディレクトリが見えています。一方でフックコマンド自体はサンドボックス化されません。スクリプトは、サンドボックスの外で動くコードとして書く必要があります。

発火する場面と発火しない場面

ディレクトリが増える経路は複数あり、発火するのは一部だけです。

発火条件

DirectoryAddedが発火する経路・しない経路

  • 発火する

    セッション中の/add-dir(matcherはslash_command)と、SDKクライアントのregister_repo_rootリクエスト(matcherはregister_repo_root)です。

  • 発火しない

    起動時の--add-dirフラグ(こちらはSessionStartの管轄)、/permissionsのWorkspaceタブからの追加、すでに作業ディレクトリである、またはその内側にあるパスの追加です。

  • 追加自体が拒否される

    UNC共有(\\server\share)や/net/<host>の自動マウントなど、ネットワークパスです。/net/<host>は、そのホストの自動マウント配下から起動した場合を除きます。シンボリックリンクやジャンクション経由でネットワーク上の場所に届くローカルパスも対象です。/add-dirは中身を調べる前に拒否するので、追加が起きずフックも動きません。

3つ目はネットワークパスの追加です。/add-dirは次のメッセージで拒否します。

\\server\share is a network path, which cannot be added as a working directory.

メッセージには続きがあり、Windowsではドライブ文字に割り当てて起動時に--add-dirで渡すよう案内されます。起動後に追加したドライブ文字には、リモート読み取りの信頼がまだ付きません。

作業ディレクトリの内側にあるサブディレクトリを/add-dirした場合は、v2.1.257以降、作業ディレクトリを増やさずにそこのSkills・コマンド・Sub-agentsだけが読み込まれます。すでに読めるパスなので、これもDirectoryAddedの対象外です。

「アクセスを制御する」の実態 — ブロックではなく取り消し

DirectoryAddedにはdecision controlがありません。追加はフックが起動する前に完了しているので、止める対象が無いのです。exit codeを変えても追加の可否には影響せず、stderrはデバッグログに行くだけです。「承認直後にアクセスを制御する」と聞くとブロックを想像しがちですが、このフックでできるのは、条件に合わない追加を検知して事後に対処することです。

additionalDirectoriesという設定キーと/add-dirは、別の経路です。/add-dirは現在のセッション中のファイルアクセスを広げるコマンドで、permissions.additionalDirectoriesに書いた設定とは区別されます。changelogにも、この区別を前提にした修正があります。

バージョンchangelogの記述
v2.1.98changelogの記述additionalDirectoriesの変更がセッション中にも反映され、外したディレクトリは即座にアクセスを失う
v2.1.98changelogの記述additionalDirectoriesから外しても、同じディレクトリを--add-dirで渡していた場合のアクセスは失効しない
v2.1.101changelogの記述/add-dir --rememberなどアプリ内の設定書き込みが、メモリ上のスナップショットを更新しない問題を修正

つまり、フックから.claude/settings.local.jsonのadditionalDirectoriesを書き換えて取り消せるのは、設定ファイルに載っているエントリです。素の/add-dirで増えたセッション限りのアクセスは、hooks・permissions・commandsのページに、設定の書き換えで失効するという記述がありません。

ファイル単位のアクセス制御は、フックが無くても動いています。追加ディレクトリ内のファイルも、通常の作業ディレクトリと同じRead/Editの許可ルールに従います。機微なパスはdenyルールで最初から塞ぎ、DirectoryAddedは追加された事実を記録・通知する監査層に置く、という分担になります。settings.jsonでのpermissions設計を先に固めておくと、フックは監査に専念できます。

v2.1.234で/add-dirがターン中でも使えるようになった

v2.1.234より前は、Claudeが応答している最中に/add-dirを実行すると、コマンドはターンの終了までキューで待たされていました。このバージョン以降は、作業中でもその場で確認が出ます。承認すれば、同じターン内の次のツール呼び出しから新しいディレクトリへ届きます。

この変更はフックの設計に響きます。フックはバックグラウンドで動き、Claude Codeは完了を待ちません。ターン中の追加は即座に有効になるため、Claudeがファイルを読みに行くタイミングと、フックが取り消しを終えるタイミングのどちらが先かは決まっていません。取り消しを前提にするなら、フックの完了までの短い時間にアクセスが起きうると見ておきます。

追加ディレクトリが一緒に持ち込む設定

/add-dirや--add-dirで追加したディレクトリは、ファイルアクセスを広げるだけでなく、一部の設定も読み込みます。permissions.additionalDirectoriesに書いただけのディレクトリは、ファイルアクセスのみで対象外です。

設定の種類追加ディレクトリからの読み込み
.claude/skills/のSkills追加ディレクトリからの読み込み読み込む(ライブリロードあり)
.claude/commands/のコマンド追加ディレクトリからの読み込み読み込む(ライブリロードなし。同名なら自プロジェクト側が優先)
.claude/agents/のSub-agents追加ディレクトリからの読み込み読み込む(ライブリロードなし)
settings.json/settings.local.json追加ディレクトリからの読み込みenabledPluginsとextraKnownMarketplacesのみ
CLAUDE.md・.claude/rules/・CLAUDE.local.md追加ディレクトリからの読み込みCLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1を設定した場合のみ

信頼できないリポジトリを/add-dirする運用なら、ここが監査の本命です。.claude/skills/と.claude/commands/の有無を調べ、想定外のファイルがあれば検知を通知する、という使い方が考えられます。

レシピ: 許可ルート外の追加を検知して通知する

フックはsettingsのhooksに登録します。matcherで追加経路を絞れ、省略すればどちらの経路でも動きます。

{
  "hooks": {
    "DirectoryAdded": [
      {
        "matcher": "slash_command",
        "hooks": [
          { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/dir-audit.sh" }
        ]
      }
    ]
  }
}

スクリプトへはstdinで次のJSONが渡ります。transcript_pathなどを含む6つのフィールドのうち、使うのはdirectory(追加された絶対パス)とsourceです。

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../transcript.jsonl",
  "cwd": "/Users/my-project",
  "hook_event_name": "DirectoryAdded",
  "directory": "/Users/my-other-repo",
  "source": "slash_command"
}

ワークスペースのルート配下だけを許可し、外れたものは設定ファイルから外して通知します。出力はsystemMessageのJSONにするので、slash_command経由なら次のターンでClaudeにコンテキストとして渡ります。

#!/bin/bash
# DirectoryAdded: 許可ルート外の追加を検知し、設定に載っていれば外す
input=$(cat)
dir=$(echo "$input" | jq -r '.directory')
source=$(echo "$input" | jq -r '.source')
allowed_root="$HOME/workspace"
 
case "$dir" in
  "$allowed_root"/*) exit 0 ;;
esac
 
settings_file="$CLAUDE_PROJECT_DIR/.claude/settings.local.json"
result="設定に該当エントリが無く、取り消していません"
 
if [ -f "$settings_file" ] && jq -e --arg d "$dir" \
  '(.permissions.additionalDirectories // []) | index($d)' \
  "$settings_file" > /dev/null; then
  tmp=$(mktemp)
  jq --arg d "$dir" '.permissions.additionalDirectories -= [$d]' \
    "$settings_file" > "$tmp" && mv "$tmp" "$settings_file" \
    && result="設定から外しました"
fi
 
jq -nc --arg m "許可外ディレクトリを検知: $dir ($source) $result" \
  '{systemMessage: $m}'
exit 0

exit 0で終えるのは、decision controlが無く、exit codeが追加の可否に影響しないためです。

なお、.claude/settings.local.jsonはサブディレクトリからの起動でも、gitリポジトリのルートから読み込まれます。$CLAUDE_PROJECT_DIRと場所がずれる場合は、スクリプトのsettings_fileをリポジトリ直下のパスに合わせます。

書き換えのjqは、キーが無いと失敗する

最初に思いつく書き方は、jq '.permissions.additionalDirectories -= [$d]'を直接かけるものです。ところがadditionalDirectoriesキーが無いファイルでは、jq 1.7.1で次のエラーになります。

$ echo '{}' | jq --arg d /tmp/x '.permissions.additionalDirectories -= [$d]'
jq: error (at <stdin>:1): null (null) and array (["/tmp/x"]) cannot be subtracted

素の/add-dirだけを使う環境では、設定ファイルにこのキーが無いのが普通です。&& mvでつないでおけば元のファイルは壊れませんが、直後の通知が「取り消しました」と言い切る作りだと、実際は何も取り消せていないのに報告だけが飛びます。上のスクリプトは、エントリの有無をindexで先に調べ、結果の文言を実際の処理に合わせています。

同じスクリプトを一時ディレクトリで動かすと、次のとおりでした。

# 設定に載っているエントリ → 外す
{"systemMessage":"許可外ディレクトリを検知: /data/x (slash_command) 設定から外しました"}
# 設定にキーが無い、またはファイルが無い → 何も書き換えない
{"systemMessage":"許可外ディレクトリを検知: /data/z (slash_command) 設定に該当エントリが無く、取り消していません"}

2行目のメッセージは、.claude/settings.local.jsonに該当エントリが無かったことを示します。素の/add-dirによるセッション限りのアクセスのほか、プロジェクトの.claude/settings.jsonやユーザー設定にエントリがある場合も、このメッセージになります。取り消せていないので、denyルールで先に塞ぐ設計か、Claudeに伝えて作業を止める運用が要ります。

追加したディレクトリは、あとからどこまで残るか

素の/add-dirによるアクセスはセッション限りです。/cdと/bgでは保たれますが、--resumeで再開しても復元されません。

/cdでセッションを別ディレクトリへ移したとき、設定ファイル由来の追加ディレクトリは、移動先のディレクトリの設定から取り直されます。v2.1.118以降は、/add-dirで現在のディレクトリを追加したセッションが--continue/--resumeの一覧で見つかります。ただし追加したディレクトリ自体は再開時に復元されず、再度/add-dirが要ります。v2.1.144以降は、/bgでバックグラウンドへ移しても追加ディレクトリが保たれます。

フックを使わずに現状だけ知りたいなら、ステータスラインのJSONに入るworkspace.added_dirsが使えます。/add-dirと--add-dirで足したディレクトリの一覧が外部スクリプトへ渡ります(v2.1.47で追加)。DirectoryAddedが「増えた瞬間」の通知なのに対し、こちらは「今の一覧」を見る手段です。

プロジェクトの.claude/settings.jsonに書かれたadditionalDirectoriesは、そのフォルダのワークスペース信頼ダイアログを承認するまで適用されません。ダイアログには、そのフォルダが付与するルールとディレクトリの一覧が出ます。一方、レシピが書き換える.claude/settings.local.jsonは通常は自分のファイルなので、この承認を経ずに許可ルールと追加ディレクトリが適用されます。gitで追跡されている場合やシンボリックリンクの場合は別です。

出力の届き先は経路で変わる

slash_command経由なら、フックのsystemMessageは次の会話ターンでClaudeにコンテキストとして渡されます。画面に表示されるのは、失敗したフックの件数だけです。詳細な失敗内容はデバッグログに残ります。

register_repo_root経由では、systemMessageも失敗内容もデバッグログにしか残らず、ユーザーには見えません。SDK統合の追加を監査したいなら、通知の経路はスクリプト側で自前に持つ必要があります。

まとめ

DirectoryAddedは追加を止める仕組みではなく、追加された事実を検知する入口です。フックには、追加の事実に加えて、取り消せたかどうかまで報告させると、監査の穴が見えます。

この記事を共有:XはてブLinkedIn