So far in this series we've covered modes, session management, and parallelization with /fleet. This post is about hooks — one of the more powerful features of Copilot CLI. Hooks let you inject your own shell scripts at key moments during a session, enabling everything from audit logging to blocking dangerous commands before they execute.
What are hooks?
Hooks are custom scripts that run at specific points during a Copilot CLI session. They receive structured JSON input describing what's happening at that moment — which tool is being called, what arguments it received, what the session context looks like — and can optionally respond with a JSON output that influences what Copilot does next.
The key thing that makes hooks different from just writing good prompts or instructions: hooks are deterministic. They execute your code at specific lifecycle points with guaranteed outcomes. Unlike instructions that guide agent behavior, a hook can guarantee that a dangerous command never runs, or that every session is logged to your audit system — regardless of how Copilot was prompted.
Hooks are stored as JSON files in .github/hooks/*.json in your repository. For Copilot CLI specifically, they are loaded automatically from your current working directory, so no extra configuration is needed to activate them.
The different hook types
The hook types I use the most are:
sessionStart
Fires when a new session begins or when an existing session is resumed. Receives the initial prompt, the current working directory, and a source field indicating whether this is a new or resumed session.
Good uses: initializing environments, validating project state, sending a notification that a session has started, writing a session start entry to an audit log.
{
"version": 1,
"hooks": {
"sessionStart": [
{ "type": "command", "bash": "./scripts/log-session-start.sh" }
]
}
}
sessionEnd
Fires when a session completes or is terminated. Mirror of sessionStart — use it to clean up temporary resources, archive session logs, or notify your team that a long-running autopilot run has finished.
preToolUse
Fires before Copilot uses any tool — bash, edit, view, and so on. The input includes the tool name and its arguments. Unlike the other hooks, preToolUse can return a decision that directly controls whether the tool runs:
{ "permissionDecision": "deny", "permissionDecisionReason": "Destructive operations require approval" }
The three possible decisions are allow, deny, and ask (which prompts the user interactively). This is how you build enforcement — blocking dangerous commands, restricting file access, requiring human review for sensitive operations.
Next to those, we have other hook types like userPromptSubmitted, postToolUse, agentStop, subagentStop and errorOccurred. I didn't find a good use case yet for these hooks, but we'll see.
Setting up your first hooks
Create a JSON file at .github/hooks/policy.json (or any name you like) in your repository:
{
"version": 1,
"hooks": {
"sessionStart": [
{ "type": "command", "bash": "./.github/hooks/scripts/log-session.sh", "timeoutSec": 10 }
],
"preToolUse": [
{ "type": "command", "bash": "./.github/hooks/scripts/enforce-policy.sh", "timeoutSec": 30 }
]
}
}
Each hook receives its context via stdin as a JSON object. Your scripts read from stdin, process it, and optionally write a JSON response to stdout.
Here's a preToolUse hook that blocks any rm -rf command before it executes:
#!/bin/bash
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.toolName')
TOOL_ARGS=$(echo "$INPUT" | jq -r '.toolArgs')
if [ "$TOOL_NAME" = "bash" ]; then
COMMAND=$(echo "$TOOL_ARGS" | jq -r '.command')
if echo "$COMMAND" | grep -qE 'rm\s+-rf'; then
echo '{"permissionDecision":"deny","permissionDecisionReason":"rm -rf is not permitted. Use a safer delete command."}'
exit 0
fi
fi
# Allow everything else
echo '{"permissionDecision":"allow"}'
Make the script executable with chmod +x and point your hooks config at it. Now no matter how Copilot is prompted — standard mode, autopilot, or via /fleet — this class of command will never execute.
For sessionStart and userPromptSubmitted hooks, you don't need to return a JSON response at all — any output is ignored. Just write your log entry and exit 0.
Important behaviors to know
Hook failures don't block execution. If a hook exits with a non-zero code or times out, Copilot logs the failure and moves on — it never blocks agent execution. This is intentional: a broken hook script shouldn't take down your workflow. The flip side is that you need to monitor hook failures actively.
Multiple hooks of the same type run in order. If you define multiple preToolUse hooks, they execute sequentially. If any of them returns "deny", the tool is blocked — you don't need all of them to agree.
Timeouts apply per hook. The default timeout is 30 seconds. For logging hooks this is plenty; keep your policy enforcement scripts lean and fast. The GitHub docs recommend keeping hook execution under 5 seconds where possible.
Never log secrets. Prompts and tool arguments can contain sensitive data. Apply redaction before writing anything to disk or forwarding to an external system. In production, consider forwarding hook events to a centralized logging system with proper access controls rather than writing local log files.
Testing your hooks locally
Before committing hooks to a shared repo, test them by piping sample input directly into your script:
# Simulate a preToolUse event
echo '{
"timestamp": 1704614600000,
"cwd": "/path/to/project",
"toolName": "bash",
"toolArgs": "{\"command\":\"rm -rf dist\",\"description\":\"Clean build directory\"}"
}' | ./.github/hooks/scripts/enforce-policy.sh
# Validate the output is valid JSON
./.github/hooks/scripts/enforce-policy.sh | jq .
This lets you iterate on hook logic quickly without needing a live Copilot session.
Who should be using hooks?
Individual developers can use hooks for personal productivity — auto-formatting files after edits, running a linter before changes are written, keeping a local audit trail.
But hooks become especially valuable for platform and DevOps teams who are rolling out Copilot CLI across an organization. With hooks you can:
- Enforce org-wide security policies without relying on developers to configure them manually
- Ensure consistent audit trails across all developer sessions
- Block access to sensitive parts of the codebase programmatically
- Provide clear, actionable messaging when a hook denies a tool call
Because hook config files live in .github/hooks/, they can be committed to each repo and version-controlled like any other policy — giving you a reviewable, auditable record of what policies are in place.
Wrapping up
Hooks are what take Copilot CLI from a smart coding assistant to an automation platform you can actually trust in a team or enterprise context. The preToolUse hook in particular is remarkably powerful — it gives you a programmable layer between Copilot's intentions and the actual execution, letting you enforce any policy you can express in a shell script.
In the next post, we'll look at delegation — how to hand off tasks and have Copilot work autonomously on your behalf.
