Hook development
Skill Poorgramer-Zack/copilot-cli-things/plugins/plugin-dev/skills/hook-development
Hook development for Copilot CLI plugins: hooks.json, preToolUse/postToolUse/agentStop/sessionStart hooks, command hooks, event-driven validation, tool blocking, policy enforcement. Use when creating or debugging plugin hooks.From its SKILL.md
npx -y skills add Poorgramer-Zack/copilot-cli-things --skill hook-developmentAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
- runs commandsInstructs the agent to run 5 commands, including `copilot --debug` and 4 more.
SKILL.md
13.6 KB, ~3.4k tokens by cl100k_base, as published. Nobody here has run it
Hook Development for Copilot CLI Plugins
Create event-driven hooks that validate operations, enforce policies, add context, and integrate external tools into Copilot CLI workflows.
- Validate tool calls before execution (preToolUse)
- React to tool results (postToolUse)
- Enforce completion standards (agentStop, subagentStop)
- Load project context (sessionStart)
- Automate workflows across the development lifecycle
Hook Types
Command Hooks
Execute commands for deterministic checks:
{
"type": "command",
"command": "bash hooks/scripts/validate.sh",
"timeout": 60000
}
Use for:
- Fast deterministic validations
- File system operations
- External tool integrations
- Performance-critical checks
Hook Configuration Formats
Plugin hooks.json Format
For plugin hooks in hooks/hooks.json, use wrapper format:
{
"description": "Brief explanation of hooks (optional)",
"hooks": {
"preToolUse": [...],
"agentStop": [...],
"sessionStart": [...]
}
}
Key points:
descriptionfield is optionalhooksfield is required wrapper containing actual hook events- This is the plugin-specific format
Example:
{
"description": "Validation hooks for code quality",
"hooks": {
"preToolUse": [
{
"matcher": "edit",
"hooks": [
{
"type": "command",
"command": "hooks/scripts/validate.sh"
}
]
}
]
}
}
Settings Format (Direct)
For user settings in .github/settings.json, use direct format:
{
"preToolUse": [...],
"agentStop": [...],
"sessionStart": [...]
}
Key points:
- No wrapper - events directly at top level
- No description field
- This is the settings format
Important: The examples below show the hook event structure that goes inside either format. For plugin hooks.json, wrap these in {"hooks": {...}}.
Hook Events
preToolUse
Execute before any tool runs. Use to approve, deny, or modify tool calls.
Example:
{
"preToolUse": [
{
"matcher": "edit|create",
"hooks": [
{
"type": "command",
"command": "bash hooks/scripts/validate-write.sh"
}
]
}
]
}
Output for preToolUse:
{
"permissionDecision": "allow|deny|ask",
"updatedInput": {"field": "modified_value"}
}
postToolUse
Execute after tool completes. Use to react to results, provide feedback, or log.
Example:
{
"postToolUse": [
{
"matcher": "edit",
"hooks": [
{
"type": "command",
"command": "bash hooks/scripts/check-edit.sh"
}
]
}
]
}
Output behavior:
- Exit 0: stdout shown in transcript
- Exit 2: stderr fed back to Copilot
agentStop
Execute when main agent considers stopping. Use to validate completeness.
Example:
{
"agentStop": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "bash hooks/scripts/verify-completion.sh"
}
]
}
]
}
Decision output:
{
"decision": "approve|block",
"reason": "Explanation"
}
subagentStop
Execute when subagent considers stopping. Use to ensure subagent completed its task.
Similar to agentStop hook, but for subagents.
userPromptSubmitted
Execute when user submits a prompt. Use to add context, validate, or block prompts.
Example:
{
"userPromptSubmitted": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "bash hooks/scripts/check-prompt.sh"
}
]
}
]
}
sessionStart
Execute when Copilot CLI session begins. Use to load context and set environment.
Example:
{
"sessionStart": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "bash hooks/scripts/load-context.sh"
}
]
}
]
}
See examples/load-context.sh for complete example.
sessionEnd
Execute when session ends. Use for cleanup, logging, and state preservation.
Hook Output Format
Standard Output (All Hooks)
{
"continue": true,
"suppressOutput": false
}
continue: If false, halt processing (default true)suppressOutput: Hide output from transcript (default false)
Exit Codes
0- Success (stdout shown in transcript)2- Blocking error (stderr fed back to Copilot)- Other - Non-blocking error
Hook Input Format
All hooks receive JSON via stdin with common fields:
{
"session_id": "abc123",
"transcript_path": "/path/to/transcript.txt",
"cwd": "/current/working/dir",
"permission_mode": "ask|allow",
"event": "preToolUse"
}
Event-specific fields:
- preToolUse/postToolUse:
toolName,toolArgs,toolResult - userPromptSubmitted:
user_prompt - agentStop/subagentStop:
reason
Access fields in prompts using $TOOL_ARGS, $TOOL_RESULT, $USER_PROMPT, etc.
Environment Variables
Available in all command hooks:
$PROJECT_DIR- Project root path$PLUGIN_ROOT- Plugin directory (use for portable paths)
Always use relative paths or $PLUGIN_ROOT in hook commands for portability:
{
"type": "command",
"command": "bash hooks/scripts/validate.sh"
}
Plugin Hook Configuration
In plugins, define hooks in hooks/hooks.json:
{
"preToolUse": [
{
"matcher": "edit|create",
"hooks": [
{
"type": "command",
"command": "bash hooks/scripts/validate-write.sh"
}
]
}
],
"agentStop": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "bash hooks/scripts/verify-completion.sh"
}
]
}
],
"sessionStart": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "bash hooks/scripts/load-context.sh",
"timeout": 10000
}
]
}
]
}
Plugin hooks merge with user's hooks and run in parallel.
Matchers
Tool Name Matching
Exact match:
"matcher": "edit"
Multiple tools:
"matcher": "read|edit|create"
Wildcard (all tools):
"matcher": "*"
Regex patterns:
"matcher": "mcp__.*__delete.*" // All MCP delete tools
Note: Matchers are case-sensitive.
Common Patterns
// All MCP tools
"matcher": "mcp__.*"
// Specific plugin's MCP tools
"matcher": "mcp__plugin_asana_.*"
// All file operations
"matcher": "read|edit|create"
// Execute commands only
"matcher": "powershell"
Security Best Practices
Input Validation
Always validate inputs in command hooks:
#!/bin/bash
set -euo pipefail
input=$(cat)
tool_name=$(echo "$input" | jq -r '.toolName')
# Validate tool name format
if [[ ! "$tool_name" =~ ^[a-zA-Z0-9_]+$ ]]; then
echo '{"decision": "deny", "reason": "Invalid tool name"}' >&2
exit 2
fi
Path Safety
Check for path traversal and sensitive files:
file_path=$(echo "$input" | jq -r '.toolArgs.file_path')
# Deny path traversal
if [[ "$file_path" == *".."* ]]; then
echo '{"decision": "deny", "reason": "Path traversal detected"}' >&2
exit 2
fi
# Deny sensitive files
if [[ "$file_path" == *".env"* ]]; then
echo '{"decision": "deny", "reason": "Sensitive file"}' >&2
exit 2
fi
See examples/validate-write.sh and examples/validate-bash.sh for complete examples.
Quote All Variables
# GOOD: Quoted
echo "$file_path"
cd "$PROJECT_DIR"
# BAD: Unquoted (injection risk)
echo $file_path
cd $PROJECT_DIR
Set Appropriate Timeouts
{
"type": "command",
"command": "bash script.sh",
"timeout": 10000
}
Defaults: Command hooks (60000ms), Prompt hooks (30000ms)
Performance Considerations
Parallel Execution
All matching hooks run in parallel:
{
"preToolUse": [
{
"matcher": "edit",
"hooks": [
{"type": "command", "command": "check1.sh"}, // Parallel
{"type": "command", "command": "check2.sh"} // Parallel
]
}
]
}
Design implications:
- Hooks don't see each other's output
- Non-deterministic ordering
- Design for independence
Optimization
- Use command hooks for quick deterministic checks
- Use prompt hooks for complex reasoning
- Cache validation results in temp files
- Minimize I/O in hot paths
Temporarily Active Hooks
Create hooks that activate conditionally by checking for a flag file or configuration:
Pattern: Flag file activation
#!/bin/bash
# Only active when flag file exists
FLAG_FILE="$PROJECT_DIR/.enable-strict-validation"
if [ ! -f "$FLAG_FILE" ]; then
# Flag not present, skip validation
exit 0
fi
# Flag present, run validation
input=$(cat)
# ... validation logic ...
Pattern: Configuration-based activation
#!/bin/bash
# Check configuration for activation
CONFIG_FILE="$PROJECT_DIR/.github/plugin-config.json"
if [ -f "$CONFIG_FILE" ]; then
enabled=$(jq -r '.strictMode // false' "$CONFIG_FILE")
if [ "$enabled" != "true" ]; then
exit 0 # Not enabled, skip
fi
fi
# Enabled, run hook logic
input=$(cat)
# ... hook logic ...
Use cases:
- Enable strict validation only when needed
- Temporary debugging hooks
- Project-specific hook behavior
- Feature flags for hooks
Best practice: Document activation mechanism in plugin README so users know how to enable/disable temporary hooks.
Hook Lifecycle and Limitations
Hooks Load at Session Start
Important: Hooks are loaded when Copilot CLI session starts. Changes to hook configuration require restarting Copilot CLI.
Cannot hot-swap hooks:
- Editing
hooks/hooks.jsonwon't affect current session - Adding new hook scripts won't be recognized
- Changing hook commands won't update
- Must restart Copilot CLI
To test hook changes:
- Edit hook configuration or scripts
- Exit Copilot CLI session
- Restart Copilot CLI
- New hook configuration loads
- Test hooks with
copilot --debug
Hook Validation at Startup
Hooks are validated when Copilot CLI starts:
- Invalid JSON in hooks.json causes loading failure
- Missing scripts cause warnings
- Syntax errors reported in debug mode
Use /hooks command to review loaded hooks in current session.
Debugging Hooks
Enable Debug Mode
copilot --debug
Look for hook registration, execution logs, input/output JSON, and timing information.
Test Hook Scripts
Test command hooks directly:
echo '{"toolName": "create", "toolArgs": {"file_path": "/test"}}' | \
bash hooks/scripts/validate.sh
echo "Exit code: $?"
Validate JSON Output
Ensure hooks output valid JSON:
output=$(./your-hook.sh < test-input.json)
echo "$output" | jq .
Quick Reference
Hook Events Summary
| Event | When | Use For |
|---|---|---|
| preToolUse | Before tool | Validation, modification |
| postToolUse | After tool | Feedback, logging |
| userPromptSubmitted | User input | Context, validation |
| agentStop | Agent stopping | Completeness check |
| subagentStop | Subagent done | Task validation |
| sessionStart | Session begins | Context loading |
| sessionEnd | Session ends | Cleanup, logging |
Best Practices
DO:
- ✅ Use command hooks for all hook logic
- ✅ Use relative paths for portability
- ✅ Validate all inputs in command hooks
- ✅ Quote all bash variables
- ✅ Set appropriate timeouts
- ✅ Return structured JSON output
- ✅ Test hooks thoroughly
DON'T:
- ❌ Use hardcoded paths
- ❌ Trust user input without validation
- ❌ Create long-running hooks
- ❌ Rely on hook execution order
- ❌ Modify global state unpredictably
- ❌ Log sensitive information
Additional Resources
Reference Files
For detailed patterns and advanced techniques, consult:
references/patterns.md- Common hook patterns (8+ proven patterns)references/migration.md- Migrating from basic to advanced hooksreferences/advanced.md- Advanced use cases and techniques
Example Hook Scripts
Working examples in examples/:
validate-write.sh- File write validation examplevalidate-bash.sh- Bash command validation exampleload-context.sh- SessionStart context loading example
Utility Scripts
Development tools in scripts/:
validate-hook-schema.sh- Validate hooks.json structure and syntaxtest-hook.sh- Test hooks with sample input before deploymenthook-linter.sh- Check hook scripts for common issues and best practices
External Resources
- Official Docs: https://docs.github.com/en/copilot
- Examples: See security-guidance plugin in marketplace
- Testing: Use
copilot --debugfor detailed logs - Validation: Use
jqto validate hook JSON output
Implementation Workflow
To implement hooks in a plugin:
- Identify events to hook into (preToolUse, agentStop, sessionStart, etc.)
- Design command hooks for deterministic checks
- Write hook configuration in
hooks/hooks.json - For command hooks, create hook scripts
- Use relative paths for all file references
- Validate configuration with
scripts/validate-hook-schema.sh hooks/hooks.json - Test hooks with
scripts/test-hook.shbefore deployment - Test in Copilot CLI with
copilot --debug - Document hooks in plugin README
Focus on command hooks for all use cases.
What ships with it: 10 files
47.8 KB alongside SKILL.md, 6 of them executable
examples/
- load-context.shruns1.6 KB
- validate-bash.shruns1.1 KB
- validate-write.shruns981 B
references/
- advanced.md10.3 KB
- migration.md8.4 KB
- patterns.md7.2 KB
scripts/
- hook-linter.shruns4.2 KB
- README.md3.6 KB
- test-hook.shruns5.3 KB
- validate-hook-schema.shruns5.1 KB