GitHub Copilot Hooks Complete Guide: Workflow Automation for Coding Agent & CLI¶
Audience: Developers and team leads building workflow automation with Copilot Coding Agent or CLI
Key Points¶
Unified Hooks Infrastructure
A single
.github/hooks/*.jsonconfiguration works across both Coding Agent and Copilot CLIDeterministic Control via preToolUse
Programmatically
allow/denytool execution. Block dangerous operations and enforce coding standards with certainty6 Lifecycle Event Types
Insert hooks at every agent lifecycle point, from session start to error handling
Clear Differentiation from Claude Code Hooks
A side-by-side comparison covering configuration format, event count, and execution model
Introduction¶
What if an agent tries to run rm -rf /? What if it runs npm publish on a production branch without asking? Custom Instructions and AGENTS.md can only "ask nicely." Hooks can stop it.
GitHub Copilot Hooks is a workflow automation feature shared by both Coding Agent and Copilot CLI. It deterministically executes custom shell commands at each lifecycle point—from session start to tool execution to error handling. Unlike prompt-based instructions, hooks fire every time the condition is met.
This article covers Copilot Hooks specifications, configuration, and implementation patterns, with a comparison to Claude Code Hooks to guide your selection1.
Let's start by clarifying where Hooks run and which plans support them.
Copilot Hooks Overview¶
Supported Platforms¶
Copilot Hooks operates in two environments with a shared configuration format.
| Platform | Runtime | Configuration Source |
|---|---|---|
| Coding Agent | GitHub Actions sandbox | Repository default branch |
| Copilot CLI | Local terminal | Current working directory |
For Coding Agent, configuration files must exist on the default branch. CLI reads from .github/hooks/*.json in the current directory.
In other words, commit one configuration file to your repository, and it works in both Coding Agent and CLI.
Supported Plans¶
Available on the following Copilot plans:
- Copilot Pro / Pro+ (Individual)
- Copilot Business (Organization)
- Copilot Enterprise
Not available on the Free plan2.
With environments and plans clarified, let's examine the six event types that Hooks provide.
The 6 Hook Event Types¶
Six event types cover the entire agent lifecycle.
| Event | Trigger | Output Handling | Primary Use |
|---|---|---|---|
| sessionStart | Session start/resume | Ignored | Environment init, audit log start |
| sessionEnd | Session end | Ignored | Cleanup, report generation |
| userPromptSubmitted | Prompt submission | Ignored | Request logging, usage analytics |
| preToolUse | Before tool execution | Controls allow/deny | Block dangerous ops, enforce security |
| postToolUse | After tool execution | Ignored | Stats collection, failure alerts |
| errorOccurred | On error | Ignored | Error logging, notifications |
Among these, preToolUse is special. By returning a permissionDecision in JSON output, it can programmatically approve or deny tool execution1. The remaining five events cannot block agent actions—they are designed for side effects like logging, notifications, and metrics.
Master preToolUse first, and you'll cover the majority of practical Hooks value. So how do you actually set this up?
Configuration Guide¶
Basic Structure¶
Create a JSON file in the .github/hooks/ directory.
{
"version": 1,
"hooks": {
"sessionStart": [
{
"type": "command",
"bash": "./scripts/session-start.sh",
"powershell": "./scripts/session-start.ps1",
"timeoutSec": 30
}
],
"preToolUse": [
{
"type": "command",
"bash": "./.github/hooks/security-check.sh"
}
],
"postToolUse": [
{
"type": "command",
"bash": "./.github/hooks/audit-log.sh"
}
]
}
}
Hook object properties:
| Property | Description | Required |
|---|---|---|
type | "command" only | Yes |
bash | Command/script path for Bash environments | Yes (Bash) |
powershell | Command/script path for PowerShell environments | Yes (Windows) |
cwd | Working directory for script execution | No |
timeoutSec | Timeout in seconds (default: 30) | No |
comment | Description of purpose | No |
Defining both bash and powershell enables cross-platform support for Linux/macOS and Windows.
stdin Input Common Fields¶
All hooks receive JSON data via stdin.
{
"timestamp": 1704614400000,
"cwd": "/path/to/project"
}
Additional fields are provided per event type (toolName, toolArgs, prompt, error, etc.). See the Hook I/O Reference section for details.
Sequential Execution of Multiple Hooks¶
Multiple hooks can be defined for the same event. They execute sequentially in array order.
{
"preToolUse": [
{ "type": "command", "bash": "./scripts/security-check.sh", "comment": "Security check (1st)" },
{ "type": "command", "bash": "./scripts/audit-log.sh", "comment": "Audit log (2nd)" },
{ "type": "command", "bash": "./scripts/metrics.sh", "comment": "Metrics collection (3rd)" }
]
}
If the first hook returns deny, subsequent hooks are skipped and the tool call is blocked.
Now that we understand the configuration mechanics, let's look at concrete implementation patterns.
Practical Examples¶
Example 1: Blocking Dangerous Commands (preToolUse)¶
preToolUse is the core feature of Copilot Hooks. Let's start with the most common pattern.
#!/bin/bash
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.toolName')
TOOL_ARGS=$(echo "$INPUT" | jq -r '.toolArgs')
# Allow anything that isn't a bash command
if [ "$TOOL_NAME" != "bash" ]; then
exit 0
fi
# Detect dangerous patterns
COMMAND=$(echo "$TOOL_ARGS" | jq -r '.command')
if echo "$COMMAND" | grep -qE '(rm -rf /|sudo|mkfs|DROP TABLE|format)'; then
jq -n \
--arg reason "Dangerous command detected: $COMMAND" \
'{permissionDecision: "deny", permissionDecisionReason: $reason}'
exit 0
fi
# Allow by default
echo '{"permissionDecision":"allow"}'
Three possible permissionDecision values:
"allow"— Permit tool execution"deny"— Block tool execution (permissionDecisionReasonis reported to the agent)"ask"— Prompt the user for confirmation
When deny is returned, the agent either seeks an alternative approach or reports to the user. This isn't a prompt-level "please don't"—it's a code-level hard stop.
Example 2: Restricting File Edit Scope (preToolUse)¶
Block edits outside specific directories. In team development, this deterministically protects areas that should not be touched.
#!/bin/bash
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.toolName')
# Only check edit tools
if [ "$TOOL_NAME" = "edit" ] || [ "$TOOL_NAME" = "create" ]; then
FILE_PATH=$(echo "$INPUT" | jq -r '.toolArgs' | jq -r '.path')
if [[ ! "$FILE_PATH" =~ ^(src/|test/|docs/) ]]; then
jq -n '{permissionDecision: "deny", permissionDecisionReason: "Can only edit files in src/, test/, or docs/ directories"}'
exit 0
fi
fi
# Allow all other tools
exit 0
Example 3: PR Diff Static Security Scan (Coding Agent-Specific, preToolUse)¶
Coding Agent autonomously creates pull requests. This can be turned to your advantage: the moment the agent tries to run gh pr create via bash, intercept the diff and run static analysis on it. If the security score falls below threshold, block the PR creation entirely.
#!/bin/bash
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.toolName')
COMMAND=$(echo "$INPUT" | jq -r '.toolArgs' | jq -r '.command // empty')
# Only target PR creation commands (gh pr create)
if [ "$TOOL_NAME" != "bash" ] || ! echo "$COMMAND" | grep -q 'gh pr create'; then
exit 0
fi
# Get diff and run through static analysis
DIFF=$(git diff origin/main --name-only 2>/dev/null)
SECRET_COUNT=$(echo "$DIFF" | xargs grep -rn \
-e 'password\s*=' \
-e 'secret\s*=' \
-e 'api_key\s*=' \
-e 'token\s*=' 2>/dev/null | wc -l)
if [ "$SECRET_COUNT" -gt 0 ]; then
jq -n \
--arg reason "Security scan failed: ${SECRET_COUNT} potential secret(s) detected in diff. Review before creating PR." \
'{permissionDecision: "deny", permissionDecisionReason: $reason}'
exit 0
fi
echo '{"permissionDecision":"allow"}'
In CLI usage, gh pr create is called intentionally. But with Coding Agent, the agent decides to create PRs autonomously. This hook fires exactly at that critical moment. Telling the agent "don't create PRs with secrets" in Custom Instructions versus blocking it here are orders of magnitude apart in reliability.
All three examples above use preToolUse—the most frequently used event in practice. So how do the other five events come into play?
Example 4: Compliance Audit Logging (All Events)¶
For enterprise compliance requirements, deploy hooks across all events to comprehensively log operations.
{
"version": 1,
"hooks": {
"sessionStart": [{ "type": "command", "bash": "./.github/hooks/audit/log-session-start.sh" }],
"userPromptSubmitted": [{ "type": "command", "bash": "./.github/hooks/audit/log-prompt.sh" }],
"preToolUse": [{ "type": "command", "bash": "./.github/hooks/audit/log-tool-use.sh" }],
"postToolUse": [{ "type": "command", "bash": "./.github/hooks/audit/log-tool-result.sh" }],
"errorOccurred": [{ "type": "command", "bash": "./.github/hooks/audit/log-error.sh" }],
"sessionEnd": [{ "type": "command", "bash": "./.github/hooks/audit/log-session-end.sh" }]
}
}
Example postToolUse logging script (JSON Lines format):
#!/bin/bash
INPUT=$(cat)
TIMESTAMP=$(echo "$INPUT" | jq -r '.timestamp')
TOOL_NAME=$(echo "$INPUT" | jq -r '.toolName')
RESULT_TYPE=$(echo "$INPUT" | jq -r '.toolResult.resultType')
jq -n \
--arg ts "$TIMESTAMP" \
--arg tool "$TOOL_NAME" \
--arg result "$RESULT_TYPE" \
'{timestamp: $ts, tool: $tool, result: $result}' >> logs/audit.jsonl
Example 5: Slack Notification Integration (errorOccurred)¶
Auto-notify Slack on errors. Particularly useful when monitoring unattended Coding Agent runs.
#!/bin/bash
INPUT=$(cat)
ERROR_MSG=$(echo "$INPUT" | jq -r '.error.message')
ERROR_NAME=$(echo "$INPUT" | jq -r '.error.name')
WEBHOOK_URL="${SLACK_WEBHOOK_URL}"
curl -s -X POST "$WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d "$(jq -n --arg msg "[$ERROR_NAME] $ERROR_MSG" '{text: ("🚨 Agent Error: " + $msg)}')"
Webhook URL Management
Never hardcode Webhook URLs in scripts. Use environment variables or GitHub Secrets.
With these implementation patterns covered, let's document the stdin input schemas used across all events.
Hook I/O Reference¶
Stdin input schemas for each event. Use this as a reference when implementing scripts.
preToolUse Input/Output
Input (stdin)
{
"timestamp": 1704614600000,
"cwd": "/path/to/project",
"toolName": "bash",
"toolArgs": "{\"command\":\"npm test\",\"description\":\"Run tests\"}"
}
Common toolName values: bash, edit, create, view, grep, glob
Output (JSON)
{
"permissionDecision": "allow",
"permissionDecisionReason": "Safe read operation"
}
postToolUse Input
{
"timestamp": 1704614700000,
"cwd": "/path/to/project",
"toolName": "bash",
"toolArgs": "{\"command\":\"npm test\"}",
"toolResult": {
"resultType": "success",
"textResultForLlm": "All tests passed (15/15)"
}
}
resultType values: "success", "failure", "denied"
sessionStart Input
{
"timestamp": 1704614400000,
"cwd": "/path/to/project",
"source": "new",
"initialPrompt": "Create a new feature"
}
source values: "new", "resume", "startup"
sessionEnd Input
{
"timestamp": 1704618000000,
"cwd": "/path/to/project",
"reason": "complete"
}
reason values: "complete", "error", "abort", "timeout", "user_exit"
errorOccurred Input
{
"timestamp": 1704614800000,
"cwd": "/path/to/project",
"error": {
"message": "Network timeout",
"name": "TimeoutError",
"stack": "TimeoutError: Network timeout\n at ..."
}
}
With the specification documented, let's compare this to Claude Code's similarly-named Hooks system.
Comparison with Claude Code Hooks¶
SmartScope also publishes a Claude Code Hooks Complete Guide. Comparing both enables informed selection based on project requirements.
Design Philosophy Differences¶
GitHub Copilot Hooks is designed as a repository-bound team-shared automation foundation. Place configuration in .github/hooks/, merge to the default branch, and it automatically activates for both Coding Agent and CLI. This mirrors the philosophy of GitHub Actions YAML workflows—the repository is the policy boundary, and code plus hook configuration are managed in the same git history. Hook changes go through code review, and rollbacks are a single git revert.
Claude Code Hooks provides a three-tier configuration: personal, project, and admin. This stems from Claude Code being designed as a local-first tool. The split between ~/.claude/settings.json (personal) and .claude/settings.json (project) enables fine-grained control like "apply company audit requirements globally while varying formatter settings per repository." Admin policy enforcement was added later; the underlying design prioritizes individual customization.
In short, Copilot's design treats the team's git repository as the trust anchor; Claude Code's design starts from the user's local environment and layers organizational controls on top. The former integrates naturally with CI/CD; the latter excels at fine-tuning the developer experience.
Feature Comparison¶
| Aspect | GitHub Copilot Hooks | Claude Code Hooks |
|---|---|---|
| Config file | .github/hooks/*.json | ~/.claude/settings.json / .claude/settings.json |
| Config format | JSON (with version key) | JSON (under hooks key) |
| Event count | 6 | 14 |
| Execution types | command only | command / prompt / agent |
| Tool execution control | preToolUse deny/allow | PreToolUse deny/allow + updatedInput |
| Tool input modification | Not supported | updatedInput supported |
| Matcher patterns | None (filter in script) | Regex matchers |
| Prompt-type hooks | Not supported | LLM-based judgment available |
| OS support | bash + powershell parallel | bash only |
| Timeout | Default 30s | Default 60s |
| Async execution | Not supported | async: true supported |
| Interactive management | None | /hooks command |
Event Correspondence¶
| GitHub Copilot | Claude Code | Notes |
|---|---|---|
| sessionStart | SessionStart | Functionally equivalent |
| sessionEnd | SessionEnd | Functionally equivalent |
| userPromptSubmitted | UserPromptSubmit | Claude supports context injection and blocking |
| preToolUse | PreToolUse | Claude adds matcher + updatedInput + prompt-type hooks |
| postToolUse | PostToolUse | Claude supports additionalContext injection |
| errorOccurred | (No equivalent) | Copilot-specific |
| (No equivalent) | PermissionRequest | Auto-respond to permission dialogs |
| (No equivalent) | Stop / SubagentStop | Stop decision control |
| (No equivalent) | SubagentStart | Sub-agent monitoring |
| (No equivalent) | PreCompact | Pre-compact processing |
| (No equivalent) | Notification | Notification event control |
| (No equivalent) | TeammateIdle / TaskCompleted | Agent Teams integration |
Decision Criteria¶
Copilot Hooks works best when:
- Your workflow centers on GitHub's Coding Agent
- You need unified security policies and audit logs across the team
- Windows support (PowerShell) is required
- You want hook configs version-controlled in the repository
Claude Code Hooks works best when:
- Real-time quality control in local development is the priority
- LLM-based judgment (Prompt hooks) or sub-agent control is needed
- You want dynamic tool input modification (updatedInput)
- You distribute hooks via plugins
Use both when:
Apply Copilot Hooks for audit logging and security on GitHub Coding Agent tasks, and Claude Code Hooks for formatting and quality checks in local Claude Code sessions. Deploy shared security scripts to both .github/hooks/ and .claude/hooks/ to maintain consistent safety standards across environments.
Whichever system you choose, the quality of your hook scripts directly determines reliability. Let's cover implementation best practices.
Script Best Practices¶
Reading and Parsing Input¶
#!/bin/bash
# Read JSON input from stdin
INPUT=$(cat)
# Extract fields with jq
TOOL_NAME=$(echo "$INPUT" | jq -r '.toolName')
TIMESTAMP=$(echo "$INPUT" | jq -r '.timestamp')
For PowerShell:
$input = [Console]::In.ReadToEnd() | ConvertFrom-Json
$toolName = $input.toolName
$timestamp = $input.timestamp
Building JSON Output¶
# ❌ String concatenation (risks parse errors with special chars)
echo "{\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"$REASON\"}"
# ✅ jq -n (safe JSON construction)
REASON="Path traversal detected"
jq -n --arg reason "$REASON" '{permissionDecision: "deny", permissionDecisionReason: $reason}'
Using jq -n prevents JSON parse errors from unescaped special characters.
Performance¶
Hooks execute synchronously and block agent processing until completion.
- Target under 5 seconds execution time
- Prefer file appends (async I/O)
- Delegate heavy processing to background processes (
nohup ... &) - Cache computation results
Security¶
- Always validate and sanitize hook inputs
- Apply proper shell escaping when constructing commands
- Never log sensitive information (tokens, passwords)
- Set appropriate permissions (
chmod 700) on hook scripts and log files
Coding Agent-Specific Considerations¶
With CLI, you can watch terminal output directly. Coding Agent runs unattended in a GitHub Actions sandbox. This difference creates real-world pitfalls worth knowing.
Sandbox Environment Constraints¶
Coding Agent sandboxes have the following constraints:
- Outbound network access is restricted. For external API calls like Slack notifications, check GitHub Actions IP allowlist settings
- Writable paths may be limited. Use relative paths within the repository for log files; avoid absolute paths
- Local files don't persist across sessions. Use Actions Cache when caching is needed
GitHub Secrets Integration¶
Store Webhook URLs and API keys in GitHub Secrets and pass them to hook scripts via Actions environment variables.
# Set environment variables in Copilot Agent setup steps
steps:
- name: Set hook environment
run: echo "SLACK_WEBHOOK_URL=${{ secrets.SLACK_WEBHOOK_URL }}" >> $GITHUB_ENV
Hook scripts reference $SLACK_WEBHOOK_URL as an environment variable. Never hardcode secrets directly in scripts—they persist in repository history.
Debugging via Actions Logs¶
How Coding Agent fails a hook is visible in GitHub Actions job logs. Since stderr output is captured directly in the log, debug echo ... >&2 statements are effective.
# Make hook behavior visible in Actions logs
echo "::debug::Processing tool: $TOOL_NAME" >&2
echo "::warning::Suspicious pattern detected in: $FILE_PATH" >&2
Using ::debug:: and ::warning:: annotation formats makes output display prominently in the Actions UI.
Troubleshooting¶
Hooks Not Executing¶
- Verify config files exist in
.github/hooks/ - For Coding Agent, confirm config files are merged to the default branch
- Check that scripts have execute permissions (
chmod +x) - Validate JSON syntax (
jq . < hooks.json)
Local Testing¶
Test hook scripts by piping sample input.
# Create test input and pipe to hook
echo '{"timestamp":1704614400000,"cwd":"/tmp","toolName":"bash","toolArgs":"{\"command\":\"ls\"}"}' \
| ./.github/hooks/security-check.sh
# Check exit code
echo $?
# Validate JSON output
./.github/hooks/security-check.sh < test-input.json | jq .
Debug Logging¶
#!/bin/bash
INPUT=$(cat)
echo "DEBUG: Received input" >&2
echo "$INPUT" >&2
# ... rest of processing
Output to stderr doesn't affect hook results, making it safe for debug information.
Summary¶
GitHub Copilot Hooks is a repository-based workflow automation foundation shared by Coding Agent and Copilot CLI. Among its six events, preToolUse is the most powerful control mechanism for programmatic approval and denial of tool execution.
Compared to Claude Code Hooks, Claude Code leads in event count and Prompt hook capabilities, while Copilot Hooks offers the simplicity of team-wide application by simply committing to a repository. The two are complementary, not competitive.
The essence of Hooks is the shift from "instruction" to "enforcement." Writing "please don't" in Custom Instructions versus returning deny in preToolUse are fundamentally different in certainty. As Coding Agent routinely creates PRs, Hooks that define "what the agent may do" in code will become as standard a repository fixture as Custom Instructions.
Related Articles¶
Custom Instructions Complete Guide
Combine Hooks + Custom Instructions for both "instructions" and "enforcement"
Pair Skills × Hooks for procedural knowledge and quality control
Claude Code Hooks Complete Guide
Detailed comparison with Claude Code's 14-event, 3-handler-type Hooks
Multi-Agent Collaboration Guide
Copilot and Claude Code usage strategies
This article is based on information available as of February 27, 2026. For the latest specifications, please refer to the GitHub Copilot official documentation.