> Hooks allow you to extend and customize the behavior of [[GitHub Copilot]] agents by executing custom shell commands at key points during agent execution — such as when a session starts, before a tool runs, or when an error occurs. 🔗 **Source**: [Using hooks with GitHub Copilot agents](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/use-hooks) · [About hooks](https://docs.github.com/en/copilot/concepts/agents/coding-agent/about-hooks) · [Hooks configuration reference](https://docs.github.com/en/copilot/reference/hooks-configuration) ## What Are Hooks? Hooks enable you to execute custom shell commands at strategic points in an agent's workflow. They receive detailed information about agent actions via JSON input on `stdin`, enabling context-aware automation. Hooks are available for: - **Copilot coding agent** on GitHub (hooks stored at `.github/hooks/*.json` on the default branch) - **GitHub Copilot CLI** in the terminal (hooks loaded from the current working directory) Common use cases include: - **Security enforcement** — programmatically approve or deny tool executions - **Audit logging** — create compliance trails of all agent actions - **Secret scanning** — prevent credential leaks before they happen - **Custom validation** — enforce coding standards and project policies - **Notifications** — alert teams on errors or sensitive operations - **Cost tracking** — monitor tool usage for resource allocation ## Hook Types There are **8 hook triggers** available: | Hook | When It Fires | Can Control Execution? | |---|---|---| | `sessionStart` | New session begins or existing session resumes | No | | `sessionEnd` | Session completes or is terminated | No | | `userPromptSubmitted` | User submits a prompt | No | | `preToolUse` | Before any tool runs (`bash`, `edit`, `view`, etc.) | ✅ Yes — can **deny** | | `postToolUse` | After a tool completes (success or failure) | No | | `agentStop` | Main agent finishes responding to a prompt | No | | `subagentStop` | Subagent completes, before returning results to parent | No | | `errorOccurred` | Error occurs during agent execution | No | > [!tip] The `preToolUse` hook is the most powerful — it can **approve or deny** tool executions, making it essential for security policies. ## Creating a Hook ### Step 1: Create the Configuration File Create a JSON file in `.github/hooks/` (e.g., `.github/hooks/my-hooks.json`): ```json { "version": 1, "hooks": { "sessionStart": [], "sessionEnd": [], "userPromptSubmitted": [], "preToolUse": [], "postToolUse": [], "errorOccurred": [] } } ``` > [!important] The file **must be present on the default branch** for Copilot coding agent to use it. For Copilot CLI, hooks are loaded from the current working directory. ### Step 2: Add Hook Definitions Each hook is an object with these properties: | Property | Required | Description | |---|---|---| | `type` | Yes | Must be `"command"` | | `bash` | Yes (Unix) | Shell command or path to bash script | | `powershell` | Yes (Windows) | PowerShell command or script path | | `cwd` | No | Working directory (relative to repo root) | | `env` | No | Additional environment variables (merged with existing) | | `timeoutSec` | No | Max execution time in seconds (default: 30) | ### Step 3: Commit to Default Branch Commit the file and merge it into the default branch. Hooks will run during agent sessions automatically. ## Hook Input & Output ### Input (All Hooks) Every hook receives a JSON object on `stdin` with at minimum: - `timestamp` — Unix timestamp in milliseconds - `cwd` — current working directory Additional fields depend on the hook type. ### Session Start Input ```json { "timestamp": 1704614400000, "cwd": "/path/to/project", "source": "new", "initialPrompt": "Create a new feature" } ``` - `source`: `"new"`, `"resume"`, or `"startup"` ### Session End Input ```json { "timestamp": 1704618000000, "cwd": "/path/to/project", "reason": "complete" } ``` - `reason`: `"complete"`, `"error"`, `"abort"`, `"timeout"`, or `"user_exit"` ### User Prompt Submitted Input ```json { "timestamp": 1704614500000, "cwd": "/path/to/project", "prompt": "Fix the authentication bug" } ``` ### Pre-Tool Use Input ```json { "timestamp": 1704614600000, "cwd": "/path/to/project", "toolName": "bash", "toolArgs": "{\"command\":\"rm -rf dist\",\"description\":\"Clean build directory\"}" } ``` ### Pre-Tool Use Output (Only Hook With Meaningful Output) ```json { "permissionDecision": "deny", "permissionDecisionReason": "Destructive operations require approval" } ``` - `permissionDecision`: `"allow"`, `"deny"`, or `"ask"` (currently only `"deny"` is actively processed) - If no output is returned, the tool is **allowed by default** ### Post-Tool Use Input ```json { "timestamp": 1704614700000, "cwd": "/path/to/project", "toolName": "bash", "toolArgs": "{\"command\":\"npm test\"}", "toolResult": { "resultType": "success", "textResultForLlm": "All tests passed (15/15)" } } ``` - `resultType`: `"success"`, `"failure"`, or `"denied"` ### Error Occurred Input ```json { "timestamp": 1704614800000, "cwd": "/path/to/project", "error": { "message": "Network timeout", "name": "TimeoutError", "stack": "TimeoutError: Network timeout\n at ..." } } ``` ## Full Configuration Example ```json { "version": 1, "hooks": { "sessionStart": [ { "type": "command", "bash": "echo \"Session started: $(date)\" >> logs/session.log", "powershell": "Add-Content -Path logs/session.log -Value \"Session started: $(Get-Date)\"", "cwd": ".", "timeoutSec": 10 } ], "userPromptSubmitted": [ { "type": "command", "bash": "./scripts/log-prompt.sh", "powershell": "./scripts/log-prompt.ps1", "cwd": "scripts", "env": { "LOG_LEVEL": "INFO" } } ], "preToolUse": [ { "type": "command", "bash": "./scripts/security-check.sh", "powershell": "./scripts/security-check.ps1", "cwd": "scripts", "timeoutSec": 15 }, { "type": "command", "bash": "./scripts/log-tool-use.sh", "powershell": "./scripts/log-tool-use.ps1", "cwd": "scripts" } ], "postToolUse": [ { "type": "command", "bash": "cat >> logs/tool-results.jsonl", "powershell": "$input | Add-Content -Path logs/tool-results.jsonl" } ], "sessionEnd": [ { "type": "command", "bash": "./scripts/cleanup.sh", "powershell": "./scripts/cleanup.ps1", "cwd": "scripts", "timeoutSec": 60 } ] } } ``` ## Writing Hook Scripts ### Reading Input (Bash) ```bash #!/bin/bash INPUT=$(cat) TIMESTAMP=$(echo "$INPUT" | jq -r '.timestamp') CWD=$(echo "$INPUT" | jq -r '.cwd') ``` ### Reading Input (PowerShell) ```powershell $input = [Console]::In.ReadToEnd() | ConvertFrom-Json $timestamp = $input.timestamp $cwd = $input.cwd ``` ### Outputting JSON ```bash # Compact single-line output with jq echo '{"permissionDecision":"deny","permissionDecisionReason":"Policy violation"}' | jq -c # Or construct dynamically REASON="Too dangerous" jq -n --arg reason "$REASON" '{permissionDecision: "deny", permissionDecisionReason: $reason}' ``` ```powershell $output = @{ permissionDecision = "deny" permissionDecisionReason = "Security policy violation" } $output | ConvertTo-Json -Compress ``` ### Error Handling ```bash #!/bin/bash set -e # Exit on error INPUT=$(cat) # ... process input ... exit 0 ``` ```powershell $ErrorActionPreference = "Stop" try { $input = [Console]::In.ReadToEnd() | ConvertFrom-Json # ... process input ... exit 0 } catch { Write-Error $_.Exception.Message exit 1 } ``` ## Practical Examples ### Block Dangerous Commands (preToolUse) ```bash #!/bin/bash INPUT=$(cat) TOOL_NAME=$(echo "$INPUT" | jq -r '.toolName') TOOL_ARGS=$(echo "$INPUT" | jq -r '.toolArgs') echo "$(date): Tool=$TOOL_NAME Args=$TOOL_ARGS" >> tool-usage.log if echo "$TOOL_ARGS" | grep -qE "rm -rf /|format|DROP TABLE"; then echo '{"permissionDecision":"deny","permissionDecisionReason":"Dangerous command detected"}' exit 0 fi ``` ### Restrict File Edits to Specific Directories ```bash #!/bin/bash INPUT=$(cat) TOOL_NAME=$(echo "$INPUT" | jq -r '.toolName') if [ "$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/"}' exit 0 fi fi ``` ### Code Quality Enforcement ```bash #!/bin/bash INPUT=$(cat) TOOL_NAME=$(echo "$INPUT" | jq -r '.toolName') if [ "$TOOL_NAME" = "edit" ] || [ "$TOOL_NAME" = "create" ]; then npm run lint-staged if [ $? -ne 0 ]; then echo '{"permissionDecision":"deny","permissionDecisionReason":"Code does not pass linting"}' fi fi ``` ### Compliance Audit Trail ```json { "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"}] } } ``` ### Slack Notification on Error ```bash #!/bin/bash INPUT=$(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\"}" ``` ## Advanced Patterns ### Multiple Hooks of the Same Type Multiple hooks for the same event execute **in order**: ```json "preToolUse": [ { "type": "command", "bash": "./scripts/security-check.sh" }, { "type": "command", "bash": "./scripts/audit-log.sh" }, { "type": "command", "bash": "./scripts/metrics.sh" } ] ``` ### Conditional Logic — Only Validate Specific Tools ```bash #!/bin/bash INPUT=$(cat) TOOL_NAME=$(echo "$INPUT" | jq -r '.toolName') # Skip non-bash tools if [ "$TOOL_NAME" != "bash" ]; then exit 0 fi COMMAND=$(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 ``` ### Structured JSON Lines Logging ```bash #!/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 ``` ## Performance Considerations Hooks run **synchronously** and block agent execution. Keep these in mind: - **Keep execution under 5 seconds** when possible - **Use async logging** (append to files rather than synchronous I/O) - **Background expensive operations** — don't block the agent - **Cache results** of expensive computations - Increase `timeoutSec` only when truly needed (default is 30s) ## Security Considerations - **Validate and sanitize** all input processed by hooks — untrusted input could cause unexpected behavior - **Use proper shell escaping** when constructing commands to prevent injection - **Never log sensitive data** such as tokens or passwords - **Set appropriate file permissions** on hook scripts and log files - **Be cautious with external network calls** — they introduce latency and potential data exposure - **Set appropriate timeouts** to prevent resource exhaustion ## Debugging ### Enable Verbose Logging ```bash #!/bin/bash set -x # Enable bash debug mode INPUT=$(cat) echo "DEBUG: Received input" >&2 echo "$INPUT" >&2 ``` ### Test Hooks Locally ```bash # Pipe test input into your hook echo '{"timestamp":1704614400000,"cwd":"/tmp","toolName":"bash","toolArgs":"{\"command\":\"ls\"}"}' | ./my-hook.sh # Check exit code echo $? # Validate output is valid JSON ./my-hook.sh | jq . ``` ## Troubleshooting | Issue | Solution | |---|---| | Hooks not executing | Verify JSON is in `.github/hooks/`, check valid JSON syntax (`jq . hooks.json`), ensure `version: 1` is set, verify script is executable (`chmod +x`) and has a proper shebang (`#!/bin/bash`) | | Hooks timing out | Increase `timeoutSec` in config, optimize script performance | | Invalid JSON output | Ensure output is a single line, use `jq -c` (Unix) or `ConvertTo-Json -Compress` (PowerShell) |