◀ Playbook index
NO.7.5

Hooks

Updated: 2026-07-22

In a nutshell

Hooks inject custom scripts into the Copilot agent execution lifecycle and capture 6 events: session start / prompt submission / pre- and post-tool execution / error / session end.

🧠 Instructions are “requests” that rely on the agent’s judgment, whereas hooks stop the execution logic itself. If you need policy enforcement, hooks are the only choice.

The 6 hook types

HookWhen it runsInput (CLI / Cloud Agent)What you can do
🟢 sessionStartNew / resume / startupsource, initialPromptLog initialization, environment setup, notification
📝 userPromptSubmittedThe moment the user submits a promptpromptPrompt audit log, keyword alerts
🛡️ preToolUseRight before tool executiontoolName, toolArgsBlock execution with deny · allow · ask
📊 postToolUseRight after tool executiontoolResultResult logging, failure notifications, statistics
💥 errorOccurredWhen the agent crashes with an errorerrorSlack/email notification, incident log
🔚 sessionEndWhen the session endsreasonCleanup, summary dispatch

🔑 Only PreToolUse can stop the agent in its tracks. Think of the other five as “observe / record / notify” hooks.

Configuration

Place one or more .json files with any name under .github/hooks/. CLI / VS Code read locally; Cloud Agent reads from .github/hooks/ on the default branch.

{
  "version": 1,
  "hooks": {
    "preToolUse": [
      {
        "type": "command",
        "bash": "./scripts/guard.sh",
        "powershell": "./scripts/guard.ps1",
        "cwd": ".",
        "timeoutSec": 30,
        "env": { "LOG_LEVEL": "INFO" }
      }
    ]
  }
}
  • 📦 Write both bash and powershell for cross-OS compatibility
  • ⏱️ Default timeout is 30 seconds. Raise timeoutSec for heavy validations
  • 🔁 Stacking multiple hooks in an array for the same event runs them top to bottom
  • 📥 Scripts receive JSON on stdin and optionally return JSON on stdout

★ Blocking specific commands (PreToolUse)

The script receives what the agent is about to run as toolName and toolArgs, and can stop execution by returning {"permissionDecision": "deny", "permissionDecisionReason": "..."}.

#!/bin/bash
# .github/hooks/scripts/guard.sh
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.toolName')
TOOL_ARGS=$(echo "$INPUT" | jq -r '.toolArgs')

# Pass through anything that isn't bash
if [ "$TOOL_NAME" != "bash" ]; then
  exit 0
fi

COMMAND=$(echo "$TOOL_ARGS" | jq -r '.command')

# 🚨 Blocklist of dangerous commands
if echo "$COMMAND" | grep -qE 'rm -rf /|sudo |mkfs|dd if=|:\(\)\{'; then
  jq -nc \
    --arg reason "Forbidden command: $COMMAND" \
    '{permissionDecision: "deny", permissionDecisionReason: $reason}'
  exit 0
fi

# 🔒 Block changes to production environments
if echo "$COMMAND" | grep -qE 'kubectl .*--context[= ]prod|terraform apply.*prod'; then
  jq -nc '{permissionDecision: "deny", permissionDecisionReason: "Changes to production require human review"}'
  exit 0
fi

# Allow everything else (no output or "allow")
exit 0

Where are hooks loaded from?

AgentWhere hooks are read fromScope
💻 Copilot CLI.github/hooks/*.json in the current directoryOnly your CLI session
🧑‍💻 VS Code agent.github/hooks/*.json in the open workspaceOnly your VS Code agent session
☁️ Copilot Cloud Agent.github/hooks/*.json on the default branch in GitHubAll Cloud Agent sessions for that repo

📝 Watch the surface differences: hooks.json uses timeout(VS Code) vs timeoutSec(CLI/Cloud), and scripts use tool_name / tool_input / hookSpecificOutput(VS Code) vs toolName / toolArgs / permissionDecision(CLI/Cloud).