How to use agent hooks for lifecycle automation

Learn how to use agent hooks to automate workflows, enforce security policies, and control agent behavior at key lifecycle points in GitHub Copilot with JSON configuration, event schemas, and cross-platform compatibility.
Author

Dario Airoldi

Published

February 20, 2026

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

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 / or DROP TABLE before 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 applyTo patterns 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.json for 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. If updatedInput doesn’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 Stop hook blocks the agent from stopping, the agent continues running and additional turns consume premium requests. Always check the stop_hook_active field 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 PreToolUse and PostToolUse hooks
  • Use the timeout property 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 stdout

3. 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 0

4. 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-Json

Python:

#!/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

  1. 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.

  2. 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_name in your script.

  3. No async execution — Hooks run synchronously. Long-running hooks block the agent’s lifecycle.

  4. Limited tool schema visibility — The updatedInput field in PreToolUse must match the tool’s expected schema. Use “Show Chat Debug View” to discover schemas.

  5. 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/*.sh

4. 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"
  }
}
EOF

Pattern 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"}}'
    ;;
esac

Pattern 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:

  1. Right-click in the Chat view and select Diagnostics
  2. Look for the hooks section to see loaded hooks and any validation errors

View hook output

To review hook output and errors during execution:

  1. Open the Output panel (Ctrl+Shift+U)
  2. 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:

  1. Type /hooks in the chat input and press Enter
  2. Select a hook event type from the list
  3. Choose an existing hook to edit or select Add new hook to create one
  4. 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

  1. Create a minimal hook that logs input to a file:

    #!/bin/bash
    cat > /tmp/hook-debug.json
    echo '{"continue": true}'
  2. Trigger the event — Start a session, invoke a tool, etc.

  3. Inspect the logged input — Review /tmp/hook-debug.json to understand the exact JSON schema your hook receives.

  4. 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.autoApprove to 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:

  1. Hooks are deterministic — They execute your code, not the model’s interpretation of guidance
  2. Eight lifecycle events cover the full agent session from start to stop
  3. PreToolUse is the most powerful event — It can block, allow, modify, or require approval for any tool invocation
  4. Always check stop_hook_active — Prevent infinite loops in Stop and SubagentStop hooks
  5. Output valid JSON to stdout only — Send diagnostic messages to stderr
  6. Cross-platform compatible — Works with Claude Code and Copilot CLI hook formats
  7. Security matters — Hooks run shell commands with VS Code’s full permissions; review scripts carefully
  8. 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.