Hooks は、エージェントがツールを呼び出す前後(PreToolUse / PostToolUse)に任意のスクリプトを差し込める仕組みです。PreToolUse で permissionDecision: deny を返せば、エージェントの動作を 止める ことができます。
12.1 — .github/hooks/hooks.json を作成する
リポジトリのルートに .github/hooks/hooks.json を新規作成し、以下をそのまま貼り付けてください:
{
"version": 1,
"hooks": {
"PreToolUse": [
{
"type": "command",
"matcher": "^(create|edit)$",
"bash": "node ./scripts/block.mjs",
"cwd": ".github/hooks",
"timeoutSec": 5
}
]
}
}
ポイント:
matcherは Copilot CLI / Cloud Agent のみで効きます —create/editの 2 つの書き込み系ツール名にマッチ(公式 hooks リファレンス)。- VS Code Copilot 拡張は
matcherを無視 し、すべてのツール呼び出しでフックが発火します(VS Code 公式ドキュメント FAQ 参照)。つまり VS Code ではスクリプト側のパスチェックだけがゲートになります。 - matcher 正規表現は自動的に
^(?:...)$で囲まれて 完全一致 で評価されます。 bashはcwdから相対実行されます。スクリプトは次のステップで作成。
12.2 — ブロック用スクリプトを作成する
.github/hooks/scripts/block.mjs を新規作成し、以下を貼り付けてください:
import { stdin, stdout } from 'node:process';
const rawPayload = await readStdin();
const payload = parseJson(rawPayload);
if (!payload) process.exit(0);
const toolName = String(payload.tool_name ?? payload.toolName ?? 'requested');
const toolInput = payload.tool_input ?? payload.toolArgs ?? {};
const normalizedInput = JSON.stringify(toolInput).replaceAll('\\\\', '/');
const protectedPath = findProtectedPath(normalizedInput);
if (!protectedPath) {
process.exit(0);
}
deny(`Repository policy: agent writes to .github/ or package-lock.json are blocked. The ${toolName} tool referenced ${protectedPath}, which is one of these protected paths.`);
async function readStdin() {
const chunks = [];
for await (const chunk of stdin) chunks.push(chunk);
return Buffer.concat(chunks).toString('utf8').trim();
}
function parseJson(raw) {
if (!raw) return null;
try { return JSON.parse(raw); } catch { return null; }
}
function findProtectedPath(value) {
const protectedPaths = [
{ label: '.github/', pattern: /(^|[\/\s"'`=:([{])(?:\.\/)?\.github(?:\/|$)/i },
{ label: 'package-lock.json', pattern: /(^|[\/\s"'`=:([{])package-lock\.json(?:$|[\/\s"'`),}\]])/i },
];
return protectedPaths.find(({ pattern }) => pattern.test(value))?.label;
}
function deny(reason) {
const additionalContext = 'Protected paths in this repository: .github/ (Copilot config, hooks, workflows) and package-lock.json (deterministic deps). The agent must NOT create, edit, write, delete, rename, or move files that touch these paths. If the user explicitly needs such a change, instruct them to make it manually.';
stdout.write(JSON.stringify({
permissionDecision: 'deny',
permissionDecisionReason: reason,
additionalContext,
hookSpecificOutput: {
hookEventName: 'PreToolUse',
permissionDecision: 'deny',
permissionDecisionReason: reason,
additionalContext,
},
}));
}
ポイント:
- 保護対象は
.github/とpackage-lock.jsonの 2 つ。 permissionDecisionReasonとadditionalContextで 保護パスの一覧と理由をエージェントに返す ので、エージェントは次のターンで自分で軌道修正できる("どこに触ってはいけないか" を知らされる)。- 出力は Copilot CLI 形式(
permissionDecision)と VS Code 形式(hookSpecificOutput)の両方を返すので、どちらの環境でも動きます。
12.3 — Copilot Chat を再起動する
Hook の設定を読み込ませるため、Copilot Chat のセッションを一度閉じて、新しいチャットを開いてください(💬 アイコン → 「+」で新規チャット)。
12.4 — エージェントを試す(わざとブロックさせる)
新しいチャットで以下を入力してください:
.github/test.md という新しいファイルを作って、中身は "hello hooks" にしてください。
期待する挙動:
- エージェントは
create/writeツールを呼び出そうとする。 - Hook が起動 →
.github/配下を検出 →permissionDecision: denyを返す。 - チャット上に「Repository policy: agents must not modify
.github/」のような拒否メッセージが表示される。 - ファイルは作成されない。
⏱ 約10分