Hooks configuration
Find information about configuring hooks for use with GitHub Copilot CLI and Copilot coding agent.
This reference article describes the available hook types with examples, including their input and output formats, script best practices, and advanced patterns for logging, security enforcement, and external integrations. For general information about creating hooks, seeUsing hooks with GitHub Copilot agents. For a tutorial on creating hooks for the CLI, seeUsing hooks with Copilot CLI for predictable, policy-compliant execution.
Hook types
Session start hook
Executed when a new agent session begins or when resuming an existing session.
Input JSON:
{ "timestamp": 1704614400000, "cwd": "/path/to/project", "source": "new", "initialPrompt": "Create a new feature"}{"timestamp":1704614400000,"cwd":"/path/to/project","source":"new","initialPrompt":"Create a new feature"}Fields:
timestamp: Unix timestamp in millisecondscwd: Current working directorysource: Either"new"(new session),"resume"(resumed session), or"startup"initialPrompt: The user's initial prompt (if provided)
Output: Ignored (no return value processed)
Example hook:
{ "type": "command", "bash": "./scripts/session-start.sh", "powershell": "./scripts/session-start.ps1", "cwd": "scripts", "timeoutSec": 30}{"type":"command","bash":"./scripts/session-start.sh","powershell":"./scripts/session-start.ps1","cwd":"scripts","timeoutSec":30}Example script (Bash):
#!/bin/bashINPUT=$(cat)SOURCE=$(echo "$INPUT" | jq -r '.source')TIMESTAMP=$(echo "$INPUT" | jq -r '.timestamp')echo "Session started from $SOURCE at $TIMESTAMP" >> session.log
#!/bin/bashINPUT=$(cat)SOURCE=$(echo "$INPUT" | jq -r '.source')TIMESTAMP=$(echo "$INPUT" | jq -r '.timestamp')echo "Session started from $SOURCE at $TIMESTAMP" >> session.logSession end hook
Executed when the agent session completes or is terminated.
Input JSON:
{ "timestamp": 1704618000000, "cwd": "/path/to/project", "reason": "complete"}{"timestamp":1704618000000,"cwd":"/path/to/project","reason":"complete"}Fields:
timestamp: Unix timestamp in millisecondscwd: Current working directoryreason: One of"complete","error","abort","timeout", or"user_exit"
Output: Ignored
Example script:
#!/bin/bashINPUT=$(cat)REASON=$(echo "$INPUT" | jq -r '.reason')echo "Session ended: $REASON" >> session.log# Cleanup temporary filesrm -rf /tmp/session-*
#!/bin/bashINPUT=$(cat)REASON=$(echo "$INPUT" | jq -r '.reason')echo "Session ended: $REASON" >> session.log#Cleanup temporary filesrm -rf /tmp/session-*User prompt submitted hook
Executed when the user submits a prompt to the agent.
Input JSON:
{ "timestamp": 1704614500000, "cwd": "/path/to/project", "prompt": "Fix the authentication bug"}{"timestamp":1704614500000,"cwd":"/path/to/project","prompt":"Fix the authentication bug"}Fields:
timestamp: Unix timestamp in millisecondscwd: Current working directoryprompt: The exact text the user submitted
Output: Ignored (prompt modification not currently supported in customer hooks)
Example script:
#!/bin/bashINPUT=$(cat)PROMPT=$(echo "$INPUT" | jq -r '.prompt')TIMESTAMP=$(echo "$INPUT" | jq -r '.timestamp')# Log to a structured fileecho "$(date -d @$((TIMESTAMP/1000))): $PROMPT" >> prompts.log
#!/bin/bashINPUT=$(cat)PROMPT=$(echo "$INPUT" | jq -r '.prompt')TIMESTAMP=$(echo "$INPUT" | jq -r '.timestamp')#Log to a structured fileecho "$(date -d @$((TIMESTAMP/1000))): $PROMPT" >> prompts.logPre-tool use hook
Executed before the agent uses any tool (such asbash,edit,view). This is the most powerful hook as it canapprove or deny tool executions.
Input JSON:
{ "timestamp": 1704614600000, "cwd": "/path/to/project", "toolName": "bash", "toolArgs": "{\"command\":\"rm -rf dist\",\"description\":\"Clean build directory\"}"}{"timestamp":1704614600000,"cwd":"/path/to/project","toolName":"bash","toolArgs":"{\"command\":\"rm -rf dist\",\"description\":\"Clean build directory\"}"}Fields:
timestamp: Unix timestamp in millisecondscwd: Current working directorytoolName: Name of the tool being invoked (such as "bash", "edit", "view", "create")toolArgs: JSON string containing the tool's arguments
Output JSON (optional):
{ "permissionDecision": "deny", "permissionDecisionReason": "Destructive operations require approval"}{"permissionDecision":"deny","permissionDecisionReason":"Destructive operations require approval"}Output fields:
permissionDecision: Either"allow","deny", or"ask"(only"deny"is currently processed)permissionDecisionReason: Human-readable explanation for the decision
Example hook to block dangerous commands:
#!/bin/bashINPUT=$(cat)TOOL_NAME=$(echo "$INPUT" | jq -r '.toolName')TOOL_ARGS=$(echo "$INPUT" | jq -r '.toolArgs')# Log the tool useecho "$(date): Tool=$TOOL_NAME Args=$TOOL_ARGS" >> tool-usage.log# Check for dangerous patternsif echo "$TOOL_ARGS" | grep -qE "rm -rf /|format|DROP TABLE"; then echo '{"permissionDecision":"deny","permissionDecisionReason":"Dangerous command detected"}' exit 0fi# Allow by default (or omit output to allow)echo '{"permissionDecision":"allow"}'#!/bin/bashINPUT=$(cat)TOOL_NAME=$(echo "$INPUT" | jq -r '.toolName')TOOL_ARGS=$(echo "$INPUT" | jq -r '.toolArgs')#Log the tool useecho "$(date): Tool=$TOOL_NAME Args=$TOOL_ARGS" >> tool-usage.log#Checkfor dangerous patternsif echo "$TOOL_ARGS" | grep -qE "rm -rf /|format|DROP TABLE"; then echo '{"permissionDecision":"deny","permissionDecisionReason":"Dangerous command detected"}' exit 0fi#Allow by default (or omit output to allow)echo '{"permissionDecision":"allow"}'Example hook to enforce file permissions:
#!/bin/bashINPUT=$(cat)TOOL_NAME=$(echo "$INPUT" | jq -r '.toolName')# Only allow editing specific directoriesif [ "$TOOL_NAME" = "edit" ]; then PATH_ARG=$(echo "$INPUT" | jq -r '.toolArgs' | jq -r '.path') if [[ ! "$PATH_ARG" =~ ^(src/|test/) ]]; then echo '{"permissionDecision":"deny","permissionDecisionReason":"Can only edit files in src/ or test/ directories"}' exit 0 fifi# Allow all other tools#!/bin/bashINPUT=$(cat)TOOL_NAME=$(echo "$INPUT" | jq -r '.toolName')#Only allow editing specific directoriesif [ "$TOOL_NAME" = "edit" ]; then PATH_ARG=$(echo "$INPUT" | jq -r '.toolArgs' | jq -r '.path') if [[ ! "$PATH_ARG" =~ ^(src/|test/) ]]; then echo '{"permissionDecision":"deny","permissionDecisionReason":"Can only edit files in src/ or test/ directories"}' exit 0 fifi#Allow all other toolsPost-tool use hook
Executed after a tool completes execution (whether successful or failed).
Example input JSON:
{ "timestamp": 1704614700000, "cwd": "/path/to/project", "toolName": "bash", "toolArgs": "{\"command\":\"npm test\"}", "toolResult": { "resultType": "success", "textResultForLlm": "All tests passed (15/15)" }}{"timestamp":1704614700000,"cwd":"/path/to/project","toolName":"bash","toolArgs":"{\"command\":\"npm test\"}","toolResult":{"resultType":"success","textResultForLlm":"All tests passed (15/15)"}}Fields:
timestamp: Unix timestamp in millisecondscwd: Current working directorytoolName: Name of the tool that was executedtoolArgs: JSON string containing the tool's argumentstoolResult: Result object containing:resultType: Either"success","failure", or"denied"textResultForLlm: The result text shown to the agent
Output: Ignored (result modification is not currently supported)
Example script that logs tool execution statistics to a CSV file:
This script logs tool execution statistics to a CSV file and sends an email alert when a tool fails.
#!/bin/bashINPUT=$(cat)TOOL_NAME=$(echo "$INPUT" | jq -r '.toolName')RESULT_TYPE=$(echo "$INPUT" | jq -r '.toolResult.resultType')# Track statisticsecho "$(date),${TOOL_NAME},${RESULT_TYPE}" >> tool-stats.csv# Alert on failuresif [ "$RESULT_TYPE" = "failure" ]; then RESULT_TEXT=$(echo "$INPUT" | jq -r '.toolResult.textResultForLlm') echo "FAILURE: $TOOL_NAME - $RESULT_TEXT" | mail -s "Agent Tool Failed" admin@example.comfi#!/bin/bashINPUT=$(cat)TOOL_NAME=$(echo "$INPUT" | jq -r '.toolName')RESULT_TYPE=$(echo "$INPUT" | jq -r '.toolResult.resultType')#Track statisticsecho "$(date),${TOOL_NAME},${RESULT_TYPE}" >> tool-stats.csv#Alert on failuresif [ "$RESULT_TYPE" = "failure" ]; then RESULT_TEXT=$(echo "$INPUT" | jq -r '.toolResult.textResultForLlm') echo "FAILURE: $TOOL_NAME - $RESULT_TEXT" | mail -s "Agent Tool Failed" admin@example.comfiError occurred hook
Executed when an error occurs during agent execution.
Example input JSON:
{ "timestamp": 1704614800000, "cwd": "/path/to/project", "error": { "message": "Network timeout", "name": "TimeoutError", "stack": "TimeoutError: Network timeout\n at ..." }}{"timestamp":1704614800000,"cwd":"/path/to/project","error":{"message":"Network timeout","name":"TimeoutError","stack":"TimeoutError: Network timeout\n at ..."}}Fields:
timestamp: Unix timestamp in millisecondscwd: Current working directoryerror: Error object containing:message: Error messagename: Error type/namestack: Stack trace (if available)
Output: Ignored (error handling modification is not currently supported)
Example script that extracts error details to a log file:
#!/bin/bashINPUT=$(cat)ERROR_MSG=$(echo "$INPUT" | jq -r '.error.message')ERROR_NAME=$(echo "$INPUT" | jq -r '.error.name')echo "$(date): [$ERROR_NAME] $ERROR_MSG" >> errors.log
#!/bin/bashINPUT=$(cat)ERROR_MSG=$(echo "$INPUT" | jq -r '.error.message')ERROR_NAME=$(echo "$INPUT" | jq -r '.error.name')echo "$(date): [$ERROR_NAME] $ERROR_MSG" >> errors.logScript best practices
Reading input
This example script reads JSON input from stdin into a variable, then usesjq to extract thetimestamp andcwd fields.
Bash:
#!/bin/bash# Read JSON from stdinINPUT=$(cat)# Parse with jqTIMESTAMP=$(echo "$INPUT" | jq -r '.timestamp')CWD=$(echo "$INPUT" | jq -r '.cwd')
#!/bin/bash#Read JSON from stdinINPUT=$(cat)#Parse with jqTIMESTAMP=$(echo "$INPUT" | jq -r '.timestamp')CWD=$(echo "$INPUT" | jq -r '.cwd')PowerShell:
# Read JSON from stdin$input = [Console]::In.ReadToEnd() | ConvertFrom-Json# Access properties$timestamp = $input.timestamp$cwd = $input.cwd
# Read JSON from stdin$input = [Console]::In.ReadToEnd() |ConvertFrom-Json# Access properties$timestamp =$input.timestamp$cwd =$input.cwdOutputting JSON
This example script shows how to output valid JSON from your hook script. Usejq -c in Bash for compact single-line output, orConvertTo-Json -Compress in PowerShell.
Bash:
#!/bin/bash# Use jq to compact the JSON output to a single lineecho '{"permissionDecision":"deny","permissionDecisionReason":"Security policy violation"}' | jq -c# Or construct with variablesREASON="Too dangerous"jq -n --arg reason "$REASON" '{permissionDecision: "deny", permissionDecisionReason: $reason}'#!/bin/bash#Use jq to compact the JSON output to a single lineecho '{"permissionDecision":"deny","permissionDecisionReason":"Security policy violation"}' | jq -c#Or construct with variablesREASON="Too dangerous"jq -n --arg reason "$REASON" '{permissionDecision: "deny", permissionDecisionReason: $reason}'PowerShell:
# Use ConvertTo-Json to compact the JSON output to a single line$output = @{ permissionDecision = "deny" permissionDecisionReason = "Security policy violation"}$output | ConvertTo-Json -Compress# Use ConvertTo-Json to compact the JSON output to a single line$output =@{ permissionDecision ="deny" permissionDecisionReason ="Security policy violation"}$output |ConvertTo-Json-CompressError handling
This script example demonstrates how to handle errors in hook scripts.
Bash:
#!/bin/bashset -e # Exit on errorINPUT=$(cat)# ... process input ...# Exit with 0 for successexit 0
#!/bin/bashset -e # Exit on errorINPUT=$(cat)#... process input ...#Exit with 0for successexit 0PowerShell:
$ErrorActionPreference = "Stop"try { $input = [Console]::In.ReadToEnd() | ConvertFrom-Json # ... process input ... exit 0} catch { Write-Error $_.Exception.Message exit 1}$ErrorActionPreference ="Stop"try {$input = [Console]::In.ReadToEnd() |ConvertFrom-Json# ... process input ...exit0}catch {Write-Error$_.Exception.Messageexit1}Handling timeouts
Hooks have a default timeout of 30 seconds. For longer operations, increasetimeoutSec:
{ "type": "command", "bash": "./scripts/slow-validation.sh", "timeoutSec": 120}{"type":"command","bash":"./scripts/slow-validation.sh","timeoutSec":120}Advanced patterns
Multiple hooks of the same type
You can define multiple hooks for the same event. They execute in order:
{ "version": 1, "hooks": { "preToolUse": [ { "type": "command", "bash": "./scripts/security-check.sh", "comment": "Security validation - runs first" }, { "type": "command", "bash": "./scripts/audit-log.sh", "comment": "Audit logging - runs second" }, { "type": "command", "bash": "./scripts/metrics.sh", "comment": "Metrics collection - runs third" } ] }}{"version":1,"hooks":{"preToolUse":[{"type":"command","bash":"./scripts/security-check.sh","comment":"Security validation - runs first"},{"type":"command","bash":"./scripts/audit-log.sh","comment":"Audit logging - runs second"},{"type":"command","bash":"./scripts/metrics.sh","comment":"Metrics collection - runs third"}]}}Conditional logic in scripts
Example: Only block specific tools
#!/bin/bashINPUT=$(cat)TOOL_NAME=$(echo "$INPUT" | jq -r '.toolName')# Only validate bash commandsif [ "$TOOL_NAME" != "bash" ]; then exit 0 # Allow all non-bash toolsfi# Check bash command for dangerous patternsCOMMAND=$(echo "$INPUT" | jq -r '.toolArgs' | jq -r '.command')if echo "$COMMAND" | grep -qE "rm -rf|sudo|mkfs"; then echo '{"permissionDecision":"deny","permissionDecisionReason":"Dangerous system command"}'fi#!/bin/bashINPUT=$(cat)TOOL_NAME=$(echo "$INPUT" | jq -r '.toolName')#Only validate bash commandsif [ "$TOOL_NAME" != "bash" ]; then exit 0 # Allow all non-bash toolsfi#Check bashcommandfor dangerous patternsCOMMAND=$(echo "$INPUT" | jq -r '.toolArgs' | jq -r '.command')if echo "$COMMAND" | grep -qE "rm -rf|sudo|mkfs"; then echo '{"permissionDecision":"deny","permissionDecisionReason":"Dangerous system command"}'fiStructured logging
Example: JSON Lines format
#!/bin/bashINPUT=$(cat)TIMESTAMP=$(echo "$INPUT" | jq -r '.timestamp')TOOL_NAME=$(echo "$INPUT" | jq -r '.toolName')RESULT_TYPE=$(echo "$INPUT" | jq -r '.toolResult.resultType')# Output structured log entryjq -n \ --arg ts "$TIMESTAMP" \ --arg tool "$TOOL_NAME" \ --arg result "$RESULT_TYPE" \ '{timestamp: $ts, tool: $tool, result: $result}' >> logs/audit.jsonl#!/bin/bashINPUT=$(cat)TIMESTAMP=$(echo "$INPUT" | jq -r '.timestamp')TOOL_NAME=$(echo "$INPUT" | jq -r '.toolName')RESULT_TYPE=$(echo "$INPUT" | jq -r '.toolResult.resultType')#Output structuredlog entryjq -n \ --arg ts "$TIMESTAMP" \ --arg tool "$TOOL_NAME" \ --arg result "$RESULT_TYPE" \ '{timestamp: $ts, tool: $tool, result: $result}' >> logs/audit.jsonlIntegration with external systems
Example: Send alerts to Slack
#!/bin/bashINPUT=$(cat)ERROR_MSG=$(echo "$INPUT" | jq -r '.error.message')WEBHOOK_URL="https://hooks.slack.com/services/YOUR/WEBHOOK/URL"curl -X POST "$WEBHOOK_URL" \ -H 'Content-Type: application/json' \ -d "{\"text\":\"Agent Error: $ERROR_MSG\"}"#!/bin/bashINPUT=$(cat)ERROR_MSG=$(echo "$INPUT" | jq -r '.error.message')WEBHOOK_URL="https://hooks.slack.com/services/YOUR/WEBHOOK/URL"curl -X POST "$WEBHOOK_URL" \ -H 'Content-Type: application/json' \ -d "{\"text\":\"Agent Error: $ERROR_MSG\"}"Example use cases
Compliance audit trail
Log all agent actions for compliance requirements by utilizing log scripts:
{ "version": 1, "hooks": { "sessionStart": [{"type": "command", "bash": "./audit/log-session-start.sh"}], "userPromptSubmitted": [{"type": "command", "bash": "./audit/log-prompt.sh"}], "preToolUse": [{"type": "command", "bash": "./audit/log-tool-use.sh"}], "postToolUse": [{"type": "command", "bash": "./audit/log-tool-result.sh"}], "sessionEnd": [{"type": "command", "bash": "./audit/log-session-end.sh"}] }}{"version":1,"hooks":{"sessionStart":[{"type":"command","bash":"./audit/log-session-start.sh"}],"userPromptSubmitted":[{"type":"command","bash":"./audit/log-prompt.sh"}],"preToolUse":[{"type":"command","bash":"./audit/log-tool-use.sh"}],"postToolUse":[{"type":"command","bash":"./audit/log-tool-result.sh"}],"sessionEnd":[{"type":"command","bash":"./audit/log-session-end.sh"}]}}Cost tracking
Track tool usage for cost allocation:
#!/bin/bashINPUT=$(cat)TOOL_NAME=$(echo "$INPUT" | jq -r '.toolName')TIMESTAMP=$(echo "$INPUT" | jq -r '.timestamp')USER=${USER:-unknown}echo "$TIMESTAMP,$USER,$TOOL_NAME" >> /var/log/copilot/usage.csv#!/bin/bashINPUT=$(cat)TOOL_NAME=$(echo "$INPUT" | jq -r '.toolName')TIMESTAMP=$(echo "$INPUT" | jq -r '.timestamp')USER=${USER:-unknown}echo "$TIMESTAMP,$USER,$TOOL_NAME" >> /var/log/copilot/usage.csvCode quality enforcement
Prevent commits that violate code standards:
#!/bin/bashINPUT=$(cat)TOOL_NAME=$(echo "$INPUT" | jq -r '.toolName')if [ "$TOOL_NAME" = "edit" ] || [ "$TOOL_NAME" = "create" ]; then # Run linter before allowing edits npm run lint-staged if [ $? -ne 0 ]; then echo '{"permissionDecision":"deny","permissionDecisionReason":"Code does not pass linting"}' fifi#!/bin/bashINPUT=$(cat)TOOL_NAME=$(echo "$INPUT" | jq -r '.toolName')if [ "$TOOL_NAME" = "edit" ] || [ "$TOOL_NAME" = "create" ]; then #Run linter before allowing edits npm run lint-staged if [ $? -ne 0 ]; then echo '{"permissionDecision":"deny","permissionDecisionReason":"Code does not pass linting"}' fifiNotification system
Send notifications on important events:
#!/bin/bashINPUT=$(cat)PROMPT=$(echo "$INPUT" | jq -r '.prompt')# Notify on production-related promptsif echo "$PROMPT" | grep -iq "production"; then echo "ALERT: Production-related prompt: $PROMPT" | mail -s "Agent Alert" team@example.comfi
#!/bin/bashINPUT=$(cat)PROMPT=$(echo "$INPUT" | jq -r '.prompt')#Notify on production-related promptsif echo "$PROMPT" | grep -iq "production"; then echo "ALERT: Production-related prompt: $PROMPT" | mail -s "Agent Alert" team@example.comfi