Maintaining Claude Code
Covers hooks, rules, settings.json, and the entity-type decision tree. Delegates to plugins for the things they do better.
Entity-type decision
Pick the right home before writing anything:
| Need | Use |
| --- | --- |
| Run automatically before/after a tool call | Hook |
| Auto-detected capability for a recurring task | Skill (use skill-creator) |
| Heavy isolated workflow | Skill with context: fork |
| Always-on behavioral guidance | CLAUDE.md (use claude-md-improver) |
| Path-specific rules | rules/ with paths: frontmatter |
| External integration | MCP server |
Hooks
Hook events
Per-session: SessionStart, SessionEnd, Setup
Per-turn: UserPromptSubmit, UserPromptExpansion, Stop, StopFailure
Per-tool: PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied, PostToolBatch
Async: FileChanged, CwdChanged, ConfigChange, InstructionsLoaded, WorktreeCreate, WorktreeRemove, Notification, SubagentStart, SubagentStop, TaskCreated, TaskCompleted, PreCompact, PostCompact, TeammateIdle, Elicitation, ElicitationResult, MessageDisplay
Exit codes
0— success, continue2— block action; stderr is shown to Claude- non-zero (other) — non-blocking warning
Hook command forms
Shell form (classic): "command": "shell command string"
Exec form (new, avoids quoting issues): "args": ["program", "arg1", "arg2"]
Output shape
Inject context with JSON on stdout:
{
"hookSpecificOutput": {
"hookEventName": "<EventName>",
"additionalContext": "..."
}
}
Additional output fields: updatedToolOutput (replace tool output, all tools), terminalSequence (emit OSC sequences for desktop notifications), sessionTitle (set session title, SessionStart only), reloadSkills: true (re-scan skill dirs, SessionStart only). duration_ms is now included in hook input for all tool events.
Common pitfalls
- Cache busting: minute-precision timestamps in
UserPromptSubmitinvalidate prompt cache. Round to the hour. - Hook script not executable:
chmod +xand verify shebang. - Reading stdin twice: drain once, parse from a variable.
- Forgetting
set -euo pipefailin bash — silent failures otherwise.
Skills — frontmatter reference
name, description, when_to_use, argument-hint, arguments, disable-model-invocation, user-invocable, allowed-tools, disallowed-tools, model, effort, context, agent, hooks, paths, shell.
Dynamic context injection: !`command` inlines command output before Claude sees skill content. Multi-line: ```! \n cmd \n ```.
Variable substitutions: $ARGUMENTS, $ARGUMENTS[N], $N, $name, ${CLAUDE_SESSION_ID}, ${CLAUDE_EFFORT}, ${CLAUDE_SKILL_DIR}.
Visibility control in settings.json: skillOverrides — set per-skill to off, user-invocable-only, or name-only.
Rules
.claude/rules/*.md files. Each can have paths: frontmatter to load only when matching files are touched. Smaller, narrower files load less context per session.
---
paths:
- "**/*.py"
---
# Python
- guidance...
When to keep something in CLAUDE.md instead: cross-cutting interaction style, project-wide commands, or rules that apply regardless of file path.
Settings.json
Audit checklist:
- Env vars: verify each is referenced in the current claude binary (
strings ~/.local/share/claude/versions/<v> | grep VAR). Undocumented does not mean dead — many flags are intentionally unlisted. - Permissions: prefer narrow over broad.
Bash(<cmd>:*)allows everything;Bash(<cmd> <safe-args>)is tighter. Always carry a deny list for secrets (~/.ssh/**,**/*.pem,~/.env*). - Plugin allow rules:
Skill(<plugin-name>)must match the actual plugin/skill identifier; typos silently fail. - Hook wiring: matchers are regex against tool names —
""matches all,"Write|Edit|Bash"is the common write-side filter.
Audit a config
- Validate YAML frontmatter on every SKILL.md and rules/*.md
- Cross-check each
Skill(...)andmcp__...permission rule against the installed plugins/servers - Strings-grep the claude binary for env vars and settings keys to flag dead ones
- Test each hook script standalone with synthetic stdin before wiring