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
| Hook | When it runs | Input (CLI / Cloud Agent) | What you can do |
|---|---|---|---|
| ๐ข sessionStart | New / resume / startup | source, initialPrompt | Log initialization, environment setup, notification |
| ๐ userPromptSubmitted | The moment the user submits a prompt | prompt | Prompt audit log, keyword alerts |
| ๐ก๏ธ preToolUse โ | Right before tool execution | toolName, toolArgs | Block execution with deny ยท allow ยท ask |
| ๐ postToolUse | Right after tool execution | toolResult | Result logging, failure notifications, statistics |
| ๐ฅ errorOccurred | When the agent crashes with an error | error | Slack/email notification, incident log |
| ๐ sessionEnd | When the session ends | reason | Cleanup, 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
bashandpowershellfor cross-OS compatibility - โฑ๏ธ Default timeout is 30 seconds. Raise
timeoutSecfor 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?
| Agent | Where hooks are read from | Scope |
|---|---|---|
| ๐ป Copilot CLI | .github/hooks/*.json in the current directory | Only your CLI session |
| ๐งโ๐ป VS Code agent | .github/hooks/*.json in the open workspace | Only your VS Code agent session |
| โ๏ธ Copilot Cloud Agent | .github/hooks/*.json on the default branch in GitHub | All Cloud Agent sessions for that repo |
๐ Watch the surface differences: hooks.json uses
timeout(VS Code) vstimeoutSec(CLI/Cloud), and scripts usetool_name/tool_input/hookSpecificOutput(VS Code) vstoolName/toolArgs/permissionDecision(CLI/Cloud).