Guide
Hooks in every AI coding agent: Claude Code, Cursor, Gemini CLI, GitHub Copilot, Kiro and Factory Droid (2026)
Where do hooks go in each AI coding agent, and how does a hook block a command?
Claude Code reads a hooks block in .claude/settings.json, Gemini CLI a hooks block in .gemini/settings.json, Cursor reads .cursor/hooks.json, Factory Droid .factory/hooks.json, GitHub Copilot .github/hooks/*.json and Kiro any JSON file in .kiro/hooks/. Every one sends the event to your script as JSON on stdin. To block a tool call, exit with code 2 in Claude Code, Cursor, Gemini CLI, Factory Droid and the Kiro CLI, or print a deny decision as JSON (Copilot uses permissionDecision, Cursor uses permission).
Also in: 简体中文 · 日本語 · 한국어 · Español · Português (Brasil)
Coding agent settings change often
Get one email a week when a setting, flag or permission rule changes, checked against the vendor docs.
Quick answer: hook file locations
- Claude Code: a hooks key in .claude/settings.json (project) or ~/.claude/settings.json (user). /hooks shows what is configured.
- Cursor: .cursor/hooks.json (project) or ~/.cursor/hooks.json (user), with "version": 1. Enterprise and team hooks sit above both.
- Gemini CLI: a hooks key in .gemini/settings.json (project), ~/.gemini/settings.json (user) or /etc/gemini-cli/settings.json (system).
- GitHub Copilot: .github/hooks/*.json for the cloud agent and Copilot CLI, plus ~/.copilot/hooks/ for the CLI. "version": 1.
- Kiro: any .json file in .kiro/hooks/ (workspace) or ~/.kiro/hooks/ (global, CLI V3), with "version": "v1".
- Factory Droid: .factory/hooks.json (project) or ~/.factory/hooks.json (user). /hooks opens the hooks manager.
How a hook blocks a tool call
- Claude Code: a PreToolUse hook that exits 2 blocks the action and sends its stderr back as the reason.
- Cursor: exit 2, or print {"permission": "deny"}. Any other non-zero code fails open unless the hook sets failClosed: true.
- Gemini CLI: exit 0 and print {"decision": "deny", "reason": "..."} (the preferred way), or exit 2 with the reason on stderr. Other codes only warn.
- GitHub Copilot: a preToolUse hook prints {"permissionDecision": "deny", "permissionDecisionReason": "..."}. Doing nothing allows the call.
- Kiro: in the CLI, exit 2 blocks PreToolUse. In the IDE, any non-zero exit blocks a Pre Tool Use hook.
- Factory Droid: exit 2 in PreToolUse blocks the tool call. Other non-zero codes are logged and Droid continues.
Same idea, different event names
- Before a tool runs: PreToolUse (Claude Code, Kiro, Factory Droid), preToolUse and beforeShellExecution (Cursor), BeforeTool (Gemini CLI), preToolUse (Copilot).
- After a tool runs: PostToolUse (Claude Code, Kiro, Factory Droid), postToolUse and afterFileEdit (Cursor), AfterTool (Gemini CLI), postToolUse (Copilot).
- Session start: SessionStart (Claude Code, Kiro, Factory Droid, Gemini CLI), sessionStart (Cursor, Copilot).
- Agent finished: Stop (Kiro, Factory Droid), stop (Cursor), agentStop (Copilot).
- Prompt submitted: UserPromptSubmit (Kiro, Factory Droid), beforeSubmitPrompt (Cursor), userPromptSubmitted (Copilot).
Same hook, four syntaxes
Claude Code: .claude/settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
}
]
}
]
}
}Cursor: .cursor/hooks.json
{
"version": 1,
"hooks": {
"beforeShellExecution": [
{
"command": ".cursor/hooks/block-risky.sh",
"matcher": "rm -rf|curl|wget"
}
]
}
}Gemini CLI: .gemini/settings.json
{
"hooks": {
"BeforeTool": [
{
"matcher": "write_file|replace",
"hooks": [
{
"name": "security-check",
"type": "command",
"command": "$GEMINI_PROJECT_DIR/.gemini/hooks/security.sh",
"timeout": 5000
}
]
}
]
}
}Kiro: .kiro/hooks/guard-shell.json
{
"version": "v1",
"hooks": [
{
"name": "Guard shell commands",
"trigger": "PreToolUse",
"matcher": "shell",
"action": {
"type": "command",
"command": "./scripts/guard.sh"
}
}
]
}Gotchas
- Gemini CLI stdout must hold only the final JSON. Invalid JSON is treated as allow.
- Gemini CLI warns before running a project hook whose name or command changed, for example after a git pull.
- Copilot passes toolArgs as a JSON string, so parse it twice. The cloud agent treats an "ask" decision as "deny" and only loads hooks from the default branch.
- Cursor project hooks run from the project root and user hooks from ~/.cursor/, so relative script paths differ.
- Kiro agent prompt actions start a new agent loop and use credits. Shell command actions do not.
- Factory Droid matchers are case sensitive regex. Use commandRegex to filter Execute calls by the command text.
Full hooks guide for each agent
- Claude Code hooks: format on save, block risky edits
- Cursor hooks.json, events and exit codes
- Gemini CLI hooks: settings.json, events and /hooks
- GitHub Copilot hooks: .github/hooks/*.json
- Kiro hooks: .kiro/hooks triggers and actions
- Factory Droid hooks.json, matchers and exit codes
- Compare: allow all commands in every AI coding agent
- Compare: settings file locations in every AI coding agent
Get coding agent settings changes by email
When a setting, flag or permission rule in this guide changes, it goes in the weekly AgentAtlas update, checked against the vendor docs. One email a week.
Sources
Other guides
Reviewed Oct 18, 2026. Settings change often; the linked vendor docs are the source of truth.