How to use agent hooks for lifecycle automation
How to use agent hooks for lifecycle automation
⚠️ Preview Feature: Agent hooks were introduced in VS Code 1.109.3 and are currently in Preview. The configuration format and behavior might change in future releases. Your organization might have disabled hooks—contact your admin for details.
Agent hooks (.github/hooks/*.json) enable you to execute custom shell commands at key lifecycle points during agent sessions.
Unlike instruction files (which guide behavior through natural language) or agent files (which define personas and tools), hooks provide deterministic, code-driven automation with guaranteed outcomes—they run your scripts, not the model’s interpretation of them.
This article explores how to configure hooks effectively, understand the eight lifecycle events, write hook scripts that control agent behavior, and integrate hooks with your existing customization stack.
Table of contents
- 🎯 Understanding agent hooks
- 📋 Hook configuration structure
- 🔄 Hook lifecycle events
- 📥 Hook input and output
- ✍️ Writing effective hook scripts
- ⚠️ Critical limitations and boundaries
- 🚫 Common pitfalls and how to avoid them
- 🎨 Advanced patterns
- 🧪 Testing and validation
- 🔒 Security considerations
- 💡 Decision framework
- 🎯 Conclusion
- 📚 References
🎯 Understanding agent hooks
What are agent hooks?
Agent hooks are JSON-configured commands that execute at specific lifecycle points during an agent session. They receive structured JSON input via stdin and can return JSON output via stdout to influence agent behavior—including blocking tool execution, injecting context, or preventing the agent from stopping.
Key characteristics
| Aspect | Description |
|---|---|
| File format | .json files with a hooks object |
| Location | .github/hooks/ (workspace) or ~/.claude/settings.json (user) |
| Activation | Automatic at lifecycle events—no user invocation needed |
| Scope | Session-wide, applies to all tool invocations and agent states |
| Visibility | Deterministic—runs your code, not the model’s interpretation |
| Cross-compatibility | Compatible with Claude Code and Copilot CLI formats |
| Execution contexts | Works across local agents, background agents, and cloud agents |
| Communication | JSON via stdin (input) and stdout (output) |
Hooks vs other customization types
The table below shows how hooks compare to other GitHub Copilot customization files. Unlike all other types, hooks run your code directly rather than influencing the model through natural language.
| Feature | Hooks | Instructions | Prompts | Agents | Skills |
|---|---|---|---|---|---|
| File | .json |
.instructions.md |
.prompt.md |
.agent.md |
SKILL.md |
| Language | Shell commands | Markdown | Markdown | Markdown | Markdown |
| Activation | Lifecycle events | File pattern | On-demand | Agent picker | Description match |
| Deterministic | ✅ Yes | ❌ No | ❌ No | ❌ No | ❌ No |
| Can block execution | ✅ Yes | ❌ No | ❌ No | ❌ No | ❌ No |
| Can modify input | ✅ Yes | ❌ No | ❌ No | ❌ No | ❌ No |
| Cross-platform | ✅ VS Code, CLI, Claude Code | ⚠️ VS Code + GitHub.com | ❌ VS Code only | ❌ VS Code only | ✅ VS Code, CLI, coding agent |
When to use hooks
✅ Use hooks for:
- Security enforcement — Block dangerous commands like
rm -rf /orDROP TABLEbefore they execute - Automated code quality — Run formatters, linters, or tests after file modifications
- Audit trails — Log every tool invocation, command execution, or file change for compliance
- Context injection — Add project-specific information, environment details, or branch metadata at session start
- Approval control — Auto-approve safe operations while requiring confirmation for sensitive ones
- Subagent governance — Track and constrain nested agent usage
❌ Don’t use hooks for:
- Coding standards — Use instruction files with
applyTopatterns instead - Task workflows — Use prompt files or skills for repeatable procedures
- Persona and role definition — Use agent files for specialized behaviors
- Natural language guidance — Use instructions; hooks don’t influence the model’s reasoning
- Complex logic requiring LLM judgment — Hooks are deterministic, not AI-driven
📋 Hook configuration structure
File locations
VS Code searches for hook configuration files in these locations, with workspace hooks taking precedence:
| Location | Path | Scope | Committed? |
|---|---|---|---|
| Workspace (recommended) | .github/hooks/*.json |
Project-specific, shared with team | ✅ Yes |
| Workspace local | .claude/settings.local.json |
Local workspace hooks | ❌ No |
| Workspace settings | .claude/settings.json |
Workspace-level hooks | ✅ Yes |
| User settings | ~/.claude/settings.json |
Personal hooks, all workspaces | ❌ No |
Tip: Use
.github/hooks/for team-shared hooks. Use~/.claude/settings.jsonfor personal hooks that apply across all your projects.
JSON configuration format
Create a JSON file with a hooks object containing arrays of hook commands for each event type:
{
"hooks": {
"PreToolUse": [
{
"type": "command",
"command": "./scripts/validate-tool.sh",
"timeout": 15
}
],
"PostToolUse": [
{
"type": "command",
"command": "npx prettier --write \"$TOOL_INPUT_FILE_PATH\""
}
],
"SessionStart": [
{
"type": "command",
"command": "./scripts/init-session.sh"
}
]
}
}Hook command properties
Each hook entry must have type: "command" and at least one command property:
| Property | Type | Required | Description |
|---|---|---|---|
type |
string | ✅ | Must be "command" |
command |
string | ✅ | Default command to run (cross-platform fallback) |
windows |
string | ❌ | Windows-specific command override |
linux |
string | ❌ | Linux-specific command override |
osx |
string | ❌ | macOS-specific command override |
cwd |
string | ❌ | Working directory (relative to repository root) |
env |
object | ❌ | Additional environment variables |
timeout |
number | ❌ | Timeout in seconds (default: 30) |
Note: OS-specific commands are selected based on the extension host platform. In remote development scenarios (SSH, Containers, WSL), this might differ from your local operating system.
OS-specific commands
Specify different commands for each operating system when your hook scripts differ by platform:
{
"hooks": {
"PostToolUse": [
{
"type": "command",
"command": "./scripts/format.sh",
"windows": "powershell -File scripts\\format.ps1",
"linux": "./scripts/format-linux.sh",
"osx": "./scripts/format-mac.sh"
}
]
}
}The execution service selects the appropriate command based on your OS. If no OS-specific command is defined, it falls back to the command property.
🔄 Hook lifecycle events
VS Code supports eight hook events that fire at specific points during an agent session. The following diagram shows when each event fires in a typical session:
Session Start
│
├── SessionStart ──────────────── Initialize resources, log session start
│
├── UserPromptSubmit ──────────── Audit user requests, inject context
│
├── PreToolUse ────────────────── Block/allow/modify tool input
│ │
│ └── [Tool executes]
│ │
│ └── PostToolUse ───────── Run formatters, log results
│
├── SubagentStart ─────────────── Track nested agents, inject context
│ │
│ └── [Subagent runs]
│ │
│ └── SubagentStop ──────── Aggregate results, cleanup
│
├── PreCompact ────────────────── Save state before context truncation
│
└── Stop ──────────────────────── Generate reports, cleanup, notify
The following table summarizes each event, when it fires, and its typical use cases:
| Event | When it fires | Typical use cases |
|---|---|---|
SessionStart |
User submits the first prompt of a new session | Initialize resources, log session start, validate project state |
UserPromptSubmit |
User submits a prompt | Audit user requests, inject system context |
PreToolUse |
Before agent invokes any tool | Block dangerous operations, require approval, modify tool input |
PostToolUse |
After tool completes successfully | Run formatters, log results, trigger follow-up actions |
SubagentStart |
Subagent is spawned | Track nested agent usage, initialize subagent resources |
SubagentStop |
Subagent completes | Aggregate results, cleanup subagent resources |
PreCompact |
Before conversation context is compacted | Export important context, save state before truncation |
Stop |
Agent session ends | Generate reports, cleanup resources, send notifications |
📥 Hook input and output
Hooks communicate with VS Code through stdin (input) and stdout (output) using JSON.
Common input fields
Every hook receives a JSON object via stdin with these common fields:
{
"timestamp": "2026-02-09T10:30:00.000Z",
"cwd": "/path/to/workspace",
"sessionId": "session-identifier",
"hookEventName": "PreToolUse",
"transcript_path": "/path/to/transcript.json"
}Common output format
All hooks can return JSON via stdout to influence agent behavior:
{
"continue": true,
"stopReason": "Security policy violation",
"systemMessage": "Operation blocked by security hook"
}| Field | Type | Description |
|---|---|---|
continue |
boolean | Set to false to stop processing (default: true) |
stopReason |
string | Reason for stopping (shown to the model) |
systemMessage |
string | Message displayed to the user |
Exit codes
The hook’s exit code determines how VS Code handles the result:
| Exit code | Behavior |
|---|---|
| 0 | Success—parse stdout as JSON |
| 2 | Blocking error—stop processing and show error to model |
| Other | Non-blocking warning—show warning to user, continue processing |
Event-specific schemas
PreToolUse
The most powerful hook event. It fires before the agent invokes any tool and can block, allow, or modify the tool invocation.
Input (in addition to common fields):
{
"tool_name": "editFiles",
"tool_input": { "files": ["src/main.ts"] },
"tool_use_id": "tool-123"
}Output — uses a hookSpecificOutput object:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Destructive command blocked by policy",
"updatedInput": { "files": ["src/safe.ts"] },
"additionalContext": "User has read-only access to production files"
}
}| Field | Values | Description |
|---|---|---|
permissionDecision |
"allow", "deny", "ask" |
Controls tool approval |
permissionDecisionReason |
string | Reason shown to the user |
updatedInput |
object | Modified tool input (optional) |
additionalContext |
string | Extra context for the model |
Permission decision priority: When multiple hooks run for the same tool invocation, the most restrictive decision wins: deny > ask > allow.
Tip: To determine the format of
updatedInput, run the command “Show Chat Debug View” and find the logged tool schema. IfupdatedInputdoesn’t match the expected schema, it’s ignored.
PostToolUse
Fires after a tool completes successfully. Use it for formatting, logging, or triggering follow-up actions.
Input (in addition to common fields):
{
"tool_name": "editFiles",
"tool_input": { "files": ["src/main.ts"] },
"tool_use_id": "tool-123",
"tool_response": "File edited successfully"
}Output:
{
"decision": "block",
"reason": "Post-processing validation failed",
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "The edited file has lint errors that need to be fixed"
}
}| Field | Values | Description |
|---|---|---|
decision |
"block" |
Block further processing (optional) |
reason |
string | Reason for blocking (shown to the model) |
hookSpecificOutput.additionalContext |
string | Extra context injected into the conversation |
SessionStart
Fires when a new agent session begins. Use it to inject project context, validate the environment, or initialize resources.
Input (in addition to common fields):
{
"source": "new"
}Output:
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "Project: my-app v2.1.0 | Branch: main | Node: v20.11.0"
}
}| Field | Type | Description |
|---|---|---|
additionalContext |
string | Context added to the agent’s conversation |
UserPromptSubmit
Fires when the user submits a prompt. Use it for auditing, filtering, or injecting system context.
Input (in addition to common fields): Includes a prompt field with the text the user submitted.
Output: Uses the common output format only.
Stop
Fires when the agent session ends. Can prevent the agent from stopping—for example, to require test execution before the session completes.
Input (in addition to common fields):
{
"stop_hook_active": false
}| Field | Type | Description |
|---|---|---|
stop_hook_active |
boolean | true when the agent is already continuing as a result of a previous stop hook. Check this value to prevent infinite loops. |
Output:
{
"hookSpecificOutput": {
"hookEventName": "Stop",
"decision": "block",
"reason": "Run the test suite before finishing"
}
}⚠️ Warning: When a
Stophook blocks the agent from stopping, the agent continues running and additional turns consume premium requests. Always check thestop_hook_activefield to prevent the agent from running indefinitely.
SubagentStart
Fires when a subagent is spawned. Use it to track nested agent usage or inject context specific to the subagent.
Input (in addition to common fields):
{
"agent_id": "subagent-456",
"agent_type": "Plan"
}| Field | Type | Description |
|---|---|---|
agent_id |
string | Unique identifier for the subagent |
agent_type |
string | The agent name (e.g., "Plan" for built-in agents, or custom agent names) |
Output:
{
"hookSpecificOutput": {
"hookEventName": "SubagentStart",
"additionalContext": "This subagent should follow the project coding guidelines"
}
}SubagentStop
Fires when a subagent completes. Can prevent the subagent from stopping—similar to the Stop hook.
Input (in addition to common fields):
{
"agent_id": "subagent-456",
"agent_type": "Plan",
"stop_hook_active": false
}Output:
{
"decision": "block",
"reason": "Verify subagent results before completing"
}PreCompact
Fires before conversation context is compacted. Use it to save important state before truncation.
Input (in addition to common fields):
{
"trigger": "auto"
}| Field | Type | Description |
|---|---|---|
trigger |
string | How the compaction was triggered. "auto" when the conversation exceeds the prompt budget. |
Output: Uses the common output format only.
✍️ Writing effective hook scripts
Core principles
1. Keep hooks fast
Hooks execute synchronously in the agent’s lifecycle. Slow hooks degrade the entire agent experience.
- Target under 5 seconds for
PreToolUseandPostToolUsehooks - Use the
timeoutproperty to set a hard limit (default: 30 seconds) - Avoid network calls unless absolutely necessary
- Cache results where possible
2. Output valid JSON
Hooks communicate via stdout. Any non-JSON output causes parsing failures.
❌ Wrong — mixing stdout with log messages:
#!/bin/bash
echo "Starting validation..." # This breaks JSON parsing!
echo '{"continue": true}'✅ Correct — log to stderr, output only JSON to stdout:
#!/bin/bash
echo "Starting validation..." >&2 # Log to stderr
echo '{"continue": true}' # Output JSON to stdout3. Handle errors gracefully
Use exit codes to communicate status. Don’t let unhandled errors crash the hook silently.
#!/bin/bash
set -e
# Validate input
if [ -z "$1" ]; then
echo '{"continue": false, "stopReason": "No input provided"}'
exit 2
fi
# Process and output result
result=$(process_input "$1" 2>/dev/null) || {
echo "Warning: processing failed" >&2
exit 1 # Non-blocking warning
}
echo "{\"continue\": true, \"systemMessage\": \"$result\"}"
exit 04. Read input from stdin
Hook scripts receive their input as JSON on stdin. Parse it properly:
Bash (with jq):
#!/bin/bash
input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name')
tool_input=$(echo "$input" | jq -r '.tool_input')
if [ "$tool_name" = "runTerminalCommand" ]; then
# Validate the command
command=$(echo "$tool_input" | jq -r '.command')
if echo "$command" | grep -qE "rm\s+-rf|DROP\s+TABLE|format\s+c:"; then
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Destructive command blocked by security policy"}}'
exit 0
fi
fi
echo '{"continue": true}'PowerShell:
$input = $Input | Out-String | ConvertFrom-Json
if ($input.tool_name -eq "runTerminalCommand") {
$command = $input.tool_input.command
if ($command -match "rm\s+-rf|DROP\s+TABLE|format\s+c:") {
@{
hookSpecificOutput = @{
hookEventName = "PreToolUse"
permissionDecision = "deny"
permissionDecisionReason = "Destructive command blocked by security policy"
}
} | ConvertTo-Json -Depth 5
exit 0
}
}
@{ continue = $true } | ConvertTo-JsonPython:
#!/usr/bin/env python3
import json
import sys
input_data = json.load(sys.stdin)
if input_data.get("tool_name") == "runTerminalCommand":
command = input_data.get("tool_input", {}).get("command", "")
dangerous_patterns = ["rm -rf", "DROP TABLE", "format c:"]
if any(pattern in command for pattern in dangerous_patterns):
json.dump({
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Destructive command blocked by security policy"
}
}, sys.stdout)
sys.exit(0)
json.dump({"continue": True}, sys.stdout)⚠️ Critical limitations and boundaries
What hooks can’t do
Hooks don’t influence reasoning — They can inject context via
additionalContext, but they can’t change how the model thinks. Use instruction files for behavioral guidance.No matcher support in VS Code (yet) — VS Code parses Claude Code’s matcher syntax but currently ignores it. Hooks apply to all tools of the matched event type, not just specific tools. Filter by
tool_namein your script.No async execution — Hooks run synchronously. Long-running hooks block the agent’s lifecycle.
Limited tool schema visibility — The
updatedInputfield inPreToolUsemust match the tool’s expected schema. Use “Show Chat Debug View” to discover schemas.No hook-to-hook communication — Each hook execution is independent. Use the filesystem or environment variables to share state between hooks if needed.
Format compatibility notes
VS Code provides cross-compatibility with other hook formats:
| Format | Compatibility |
|---|---|
| Claude Code | VS Code parses the format, including matchers (ignored). Empty string matcher ("") represents all tools. |
| Copilot CLI | VS Code converts lowerCamelCase event names (e.g., preToolUse) to PascalCase (PreToolUse). Both bash and powershell command formats are supported. |
🚫 Common pitfalls and how to avoid them
1. Writing to stdout before the JSON response
Problem: Any text on stdout before your JSON output causes parse failures.
# ❌ This breaks everything
echo "Processing..."
echo '{"continue": true}'Fix: Send diagnostic messages to stderr:
# ✅ Correct
echo "Processing..." >&2
echo '{"continue": true}'2. Forgetting to check stop_hook_active
Problem: A Stop hook that always blocks creates an infinite loop, consuming premium requests indefinitely.
{
"hookSpecificOutput": {
"hookEventName": "Stop",
"decision": "block",
"reason": "Run tests first"
}
}Fix: Check stop_hook_active in your script:
input=$(cat)
stop_active=$(echo "$input" | jq -r '.stop_hook_active')
if [ "$stop_active" = "true" ]; then
echo '{"continue": true}' # Don't block again
exit 0
fi
# Only block on first stop
echo '{"hookSpecificOutput":{"hookEventName":"Stop","decision":"block","reason":"Run tests first"}}'3. Not setting execute permissions
Problem: Hook script fails with “Permission denied” on Linux/macOS.
Fix: Set execute permissions on your scripts:
chmod +x .github/hooks/scripts/*.sh4. Timeout issues
Problem: Complex hooks exceed the default 30-second timeout and get killed.
Fix: Set an appropriate timeout and optimize your script:
{
"hooks": {
"PostToolUse": [
{
"type": "command",
"command": "./scripts/run-tests.sh",
"timeout": 120
}
]
}
}5. Hardcoding credentials in hook scripts
Problem: Secrets committed to version control via hook scripts.
Fix: Use environment variables or the env property:
{
"hooks": {
"SessionStart": [
{
"type": "command",
"command": "./scripts/init.sh",
"env": {
"API_ENDPOINT": "https://internal.example.com"
}
}
]
}
}For secrets, use your OS credential store or a .env file that’s gitignored.
🎨 Advanced patterns
Pattern 1: Block dangerous terminal commands
{
"hooks": {
"PreToolUse": [
{
"type": "command",
"command": "./scripts/block-dangerous-commands.sh",
"windows": "powershell -File scripts\\block-dangerous-commands.ps1"
}
]
}
}The script inspects tool_name and tool_input to detect patterns like rm -rf, DROP TABLE, or format c: and returns a deny permission decision.
Pattern 2: Auto-format code after edits
{
"hooks": {
"PostToolUse": [
{
"type": "command",
"command": "npx prettier --write \"$TOOL_INPUT_FILE_PATH\""
}
]
}
}After any file edit, Prettier automatically formats the modified file—ensuring consistent styling without relying on the model to follow formatting rules.
Pattern 3: Inject project context at session start
{
"hooks": {
"SessionStart": [
{
"type": "command",
"command": "./scripts/inject-context.sh"
}
]
}
}Script example — injects branch, Node version, and project metadata:
#!/bin/bash
branch=$(git branch --show-current 2>/dev/null || echo "unknown")
node_version=$(node --version 2>/dev/null || echo "unknown")
project=$(jq -r '.name // "unnamed"' package.json 2>/dev/null || echo "unnamed")
cat <<EOF
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "Project: $project | Branch: $branch | Node: $node_version"
}
}
EOFPattern 4: Log tool usage for auditing
{
"hooks": {
"PreToolUse": [
{
"type": "command",
"command": "./scripts/audit-log.sh"
}
]
}
}The script appends tool invocation details (timestamp, tool name, input) to a log file for compliance and debugging.
Pattern 5: Require approval for specific tools
Filter by tool_name in your script and return "permissionDecision": "ask" for sensitive operations:
#!/bin/bash
input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name')
case "$tool_name" in
runTerminalCommand|deleteFiles)
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"Sensitive operation requires approval"}}'
;;
*)
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'
;;
esacPattern 6: Enforce test execution before session end
Combine Stop and SubagentStop hooks to require test execution:
#!/bin/bash
input=$(cat)
stop_active=$(echo "$input" | jq -r '.stop_hook_active')
# Prevent infinite loops
if [ "$stop_active" = "true" ]; then
echo '{"continue": true}'
exit 0
fi
# Check if tests were run during this session
if [ ! -f ".hook-state/tests-passed" ]; then
echo '{"hookSpecificOutput":{"hookEventName":"Stop","decision":"block","reason":"Run the test suite before finishing. Execute: npm test"}}'
exit 0
fi
echo '{"continue": true}'🧪 Testing and validation
View hook diagnostics
To see which hooks are loaded and check for configuration errors:
- Right-click in the Chat view and select Diagnostics
- Look for the hooks section to see loaded hooks and any validation errors
View hook output
To review hook output and errors during execution:
- Open the Output panel (
Ctrl+Shift+U) - Select GitHub Copilot Chat Hooks from the channel dropdown
Configure hooks with /hooks
Use the /hooks slash command in chat to configure hooks through an interactive UI:
- Type
/hooksin the chat input and press Enter - Select a hook event type from the list
- Choose an existing hook to edit or select Add new hook to create one
- Select or create a hook configuration file
The command opens the hook file in the editor with your cursor positioned at the command field, ready for editing.
Manual testing workflow
Create a minimal hook that logs input to a file:
#!/bin/bash cat > /tmp/hook-debug.json echo '{"continue": true}'Trigger the event — Start a session, invoke a tool, etc.
Inspect the logged input — Review
/tmp/hook-debug.jsonto understand the exact JSON schema your hook receives.Iterate — Build your logic against the real input format.
🔒 Security considerations
⚠️ Hooks execute shell commands with the same permissions as VS Code. Review hook configurations carefully, especially when using hooks from untrusted sources.
Best practices
| Practice | Reason |
|---|---|
| Review hook scripts before enabling | Especially in shared repositories—hooks run arbitrary shell commands |
| Limit hook permissions | Apply the principle of least privilege; hooks should only access what they need |
| Validate and sanitize input | Hook scripts receive input from the agent; validate it to prevent injection attacks |
| Never hardcode secrets | Use environment variables or secure credential storage |
| Protect hook scripts from agent edits | Use chat.tools.edits.autoApprove to disallow the agent from editing hook scripts without manual approval |
Agent self-modification risk
If the agent has access to edit scripts run by hooks, it can modify those scripts during its own run and execute the code it writes. To mitigate this:
- Configure
chat.tools.edits.autoApproveto require confirmation for hook script edits - Place hook scripts in a location the agent can’t easily find or modify
- Use read-only file permissions on critical hook scripts
💡 Decision framework
Use this decision tree to determine whether hooks are the right solution:
Do you need DETERMINISTIC behavior?
│
YES─ Does it involve intercepting tool execution?
│ │
│ YES─ ✅ USE PreToolUse / PostToolUse HOOKS
│ │
│ NO─ Does it involve session lifecycle?
│ │
│ YES─ ✅ USE SessionStart / Stop HOOKS
│ │
│ NO─ Does it need to run at a specific lifecycle point?
│ │
│ YES─ ✅ USE THE APPROPRIATE HOOK EVENT
│ │
│ NO─ Consider VS Code tasks or scripts
│
NO─ Do you need to influence model behavior?
│
YES─ Is it file-specific?
│ │
│ YES─ ✅ USE INSTRUCTION FILES (applyTo patterns)
│ │
│ NO─ Is it a reusable workflow with templates?
│ │
│ YES─ ✅ USE SKILLS
│ │
│ NO─ ✅ USE PROMPT or AGENT FILES
│
NO─ ❓ RECONSIDER: Maybe not needed?
Quick reference table
| Need | Solution | File type |
|---|---|---|
| Block dangerous commands | Hook | .github/hooks/security.json |
| Auto-format after edits | Hook | .github/hooks/formatting.json |
| Log all tool invocations | Hook | .github/hooks/audit.json |
| Inject project context | Hook | .github/hooks/context.json |
| Enforce tests before stopping | Hook | .github/hooks/quality.json |
| TypeScript coding standards | Instruction | .instructions.md |
| Generate React component | Prompt | .prompt.md |
| Security reviewer persona | Agent | .agent.md |
| Testing workflow with templates | Skill | SKILL.md |
🎯 Conclusion
Agent hooks fill a critical gap in the Copilot customization stack by providing deterministic, code-driven automation at key lifecycle points. While instruction files, prompts, agents, and skills all influence the model through natural language, hooks guarantee execution—your scripts run exactly as written, every time.
Key takeaways:
- Hooks are deterministic — They execute your code, not the model’s interpretation of guidance
- Eight lifecycle events cover the full agent session from start to stop
PreToolUseis the most powerful event — It can block, allow, modify, or require approval for any tool invocation- Always check
stop_hook_active— Prevent infinite loops inStopandSubagentStophooks - Output valid JSON to stdout only — Send diagnostic messages to stderr
- Cross-platform compatible — Works with Claude Code and Copilot CLI hook formats
- Security matters — Hooks run shell commands with VS Code’s full permissions; review scripts carefully
- Don’t overuse hooks — They’re for deterministic automation, not behavioral guidance; use instructions for that
By combining hooks with instruction files, prompts, agents, and skills, you can build a comprehensive customization system where the model follows your natural language guidance and your deterministic policies are enforced programmatically.
📚 References
Official documentation
VS Code: Agent Hooks [📘 Official]
Microsoft’s official documentation for agent hooks in VS Code. Covers the complete lifecycle event system, JSON configuration format, input/output schemas for all eight events, security considerations, and troubleshooting. The primary reference for understanding hooks.
VS Code: Copilot Customization Overview [📘 Official]
Comprehensive overview of all Copilot customization options including instructions, prompts, agents, skills, and hooks. Explains how different customization types work together and when to use each.
VS Code: Use Tools with Agents [📘 Official]
Documentation on tool approval and execution in VS Code. Hooks interact directly with tool invocations—understanding tool schemas helps write effective PreToolUse and PostToolUse hooks.
VS Code: Subagents [📘 Official]
Documentation on subagent delegation. Relevant for understanding SubagentStart and SubagentStop hook events and how hooks can govern nested agent behavior.
VS Code: Enterprise Policies [📘 Official]
Enterprise-level policies that can disable hooks. Understanding organizational controls helps teams deploy hooks safely.
Community resources
GitHub: Awesome Copilot Repository [📗 Verified Community]
Community-curated collection of Copilot customizations. Browse for real-world examples of hook scripts and patterns across different teams and organizations.
GitHub Blog: How to Use GitHub Copilot [📗 Verified Community]
Official GitHub blog post on maximizing Copilot effectiveness. While focused on general usage, provides context on how hooks complement other customization strategies.