| name | claude-code-developer |
| description | Reference for developing Claude Code extensions — hooks, settings, MCP, CLI, and skills. Use when building tools on Claude Code, configuring automation, debugging hooks, or understanding extension points. |
Claude Code Developer Guide
Reference for building tools and automations on top of Claude Code. Covers hooks, settings, MCP integration, and CLI usage.
Hooks
Hooks run shell commands at specific points in Claude Code's lifecycle. Configured in settings.json under hooks.<EventName>.
Hook Event Reference
| Event | Matcher | Fires when | Use for |
|---|
SessionStart | — | Session starts | Auto-register, init workspace |
SessionEnd | — | Session truly ends | Cleanup, unregister |
Stop | — | Claude stops (clear/resume/compact too) | Logging — prefer SessionEnd for cleanup |
UserPromptSubmit | — | User submits a prompt | Pre-processing, logging |
PreToolUse | Tool name | Before a tool runs | Validation, blocking dangerous ops |
PostToolUse | Tool name | After a tool succeeds | Formatting, logging, notifications |
PostToolUseFailure | Tool name | After a tool fails | Error recovery |
PreCompact | "manual"/"auto" | Before context compaction | Save state, warn user |
PostCompact | "manual"/"auto" | After compaction | Post-processing |
Notification | Notification type | On notifications | Custom notification handling |
PermissionRequest | Tool name | Before permission prompt | Custom permission logic |
SubagentStart | — | Subagent spawns | Agent lifecycle tracking |
SubagentStop | — | Subagent exits | Agent lifecycle tracking |
Hook Structure
{
"hooks": {
"EVENT_NAME": [
{
"matcher": "ToolName|OtherTool",
"hooks": [
{
"type": "command",
"command": "your-command-here",
"timeout": 60
}
]
}
]
}
}
- matcher: tool name (
"Bash", "Write", "Edit"), pipe-separated list ("Write|Edit"), or "" to match all. For non-tool events, omit or use "".
- hooks: array of hook objects, each with
type and type-specific fields.
Hook Types
command — Shell command:
{ "type": "command", "command": "echo 'hello'", "timeout": 30 }
- Receives JSON on stdin with event context
timeout in seconds (optional)
statusMessage for spinner text (optional)
once: auto-remove after first run (optional)
async: run in background without blocking (optional)
prompt — LLM evaluation (PreToolUse/PostToolUse/PermissionRequest only):
{ "type": "prompt", "prompt": "Is this safe? $ARGUMENTS" }
$ARGUMENTS placeholder replaced with hook input JSON
agent — Agent with tools (PreToolUse/PostToolUse/PermissionRequest only):
{ "type": "agent", "prompt": "Verify tests pass", "timeout": 60 }
http — POST hook input to a URL:
{ "type": "http", "url": "https://example.com/hook", "headers": {} }
mcp_tool — Call an MCP tool:
{ "type": "mcp_tool", "server": "server-name", "tool": "tool_name", "input": {} }
Hook stdin JSON
Hooks receive context on stdin. Key fields:
{
"session_id": "abc123",
"tool_name": "Write",
"tool_input": { "file_path": "/path/to/file.txt", "content": "..." },
"tool_response": { "success": true }
}
Extract with jq:
jq -r '.tool_input.file_path'
jq -r '.tool_input.command'
Hook JSON Output
Commands can output JSON to control behavior:
{
"systemMessage": "Warning shown to user",
"continue": false,
"stopReason": "Blocked because...",
"decision": "block",
"reason": "Explanation",
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "Injected into model context",
"permissionDecision": "allow",
"permissionDecisionReason": "Why allowed"
}
}
Common Hook Pitfalls
Stop vs SessionEnd: Stop fires on clear/resume/compact — use SessionEnd for cleanup
- Matcher is required: each hook entry needs
"matcher" (use "" for all)
- Nested hooks array: the outer
hooks object contains arrays of {matcher, hooks:[...]}
- Silent failure: invalid JSON in settings.json silently disables ALL settings from that file
- Hook command format:
{matcher: "", hooks: [{type: "command", command: "..."}]} — NOT flat {matcher: "", command: "..."}
Settings
File Locations
| File | Scope | Git | Use for |
|---|
~/.claude/settings.json | Global (user) | No | Personal preferences |
.claude/settings.json | Project | Yes | Team-wide config |
.claude/settings.local.json | Project local | Gitignore | Personal overrides |
Priority: user → project → local (later overrides earlier).
Key Settings for Tool Developers
Permissions — allow/deny tool operations:
{
"permissions": {
"allow": ["Bash(npm *)", "Bash(git *)"],
"deny": ["Bash(rm -rf *)"],
"defaultMode": "default"
}
}
MCP Servers — manage server approval:
{
"enableAllProjectMcpServers": true,
"enabledMcpjsonServers": ["orchestrator"],
"disabledMcpjsonServers": ["blocked-server"]
}
Environment Variables:
{ "env": { "DEBUG": "true", "ZK_HOSTS": "127.0.0.1:2181" } }
Model & Agent:
{ "model": "sonnet", "agent": "agent-name" }
Plugins:
{ "enabledPlugins": { "formatter@anthropic-tools": true } }
Status Line — custom terminal status bar:
{
"statusLine": {
"type": "command",
"command": "echo '$(date +%H:%M)'",
"padding": 0
}
}
JSON Validation
Broken JSON in settings.json silently disables the entire file. Validate with:
jq -e '.hooks.SessionStart' .claude/settings.json
python3 -m json.tool .claude/settings.json > /dev/null
MCP Integration
.mcp.json Format
{
"mcpServers": {
"server-name": {
"type": "http",
"url": "http://127.0.0.1:3100/mcp",
"headers": {
"X-Instance-Name": "my-instance",
"X-Instance-Role": "developer"
}
}
}
}
type: "http" for Streamable HTTP, "stdio" for subprocess
headers: custom HTTP headers sent with every request
- MCP tools appear as callable tools in Claude Code
MCP Server Best Practices
- Use Streamable HTTP transport on
127.0.0.1 for local servers
- Expose a
/health endpoint for diagnostics
- Provide REST endpoints alongside MCP tools for CLI-based interaction
- Use
resources/subscribe for push notifications to Claude Code
- Stateless sessions simplify scaling (set
sessionIdGenerator: undefined)
CLI Reference
Key Commands
claude
claude -p "prompt"
claude -c
claude -r
claude --resume <id>
claude --model sonnet
claude --mcp-config file.json
claude --settings file.json
claude --add-dir /path
claude --debug
claude --debug hooks
claude --bare
Useful Subcommands
claude mcp
claude auth
claude doctor
claude update
claude agents
claude plugin
Hook Testing
claude --debug hooks
claude --include-hook-events --output-format stream-json -p "test"
jq -e '.hooks.<event>[] | select(.matcher == "<matcher>") | .hooks[] | select(.type == "command") | .command' .claude/settings.json
Skills
Skill File Format
Skills live in skills/<name>/SKILL.md:
---
name: skill-name
description: One-line description — used for trigger matching
---
# Skill Title
Skill content with instructions for the LLM.
name: unique identifier, used for /skill-name invocation
description: used by Claude Code to auto-detect when to invoke the skill
Skill Design Guidelines
- Clear triggers in description — mention keywords users will say
- Step-by-step instructions — the LLM follows the skill literally
- Include bash commands — for the LLM to run
- Handle edge cases — re-registration, cleanup, verification
- Keep it concise — skills are loaded into context
Development Workflow
Testing a Hook
- Write the hook in
.claude/settings.json
- Validate JSON:
jq -e '.hooks' .claude/settings.json
- Reload config: open
/hooks in Claude Code or restart
- Test with
--debug hooks to see execution logs
- For tool hooks: trigger the tool and verify the side effect
- For lifecycle hooks (
SessionStart, SessionEnd, Stop): restart Claude Code
Troubleshooting
| Symptom | Likely cause |
|---|
| Hook doesn't fire | Invalid JSON, wrong event name, matcher mismatch |
| All settings ignored | Syntax error in settings.json |
| Hook fires but command fails | Command not in PATH, permission denied, timeout |
Stop fires unexpectedly | Stop triggers on clear/compact too — use SessionEnd |
| MCP tools not showing | Server not running, wrong URL in mcp.json, port conflict |