In previous posts, we’ve covered how the tool-calling loop works, what a production-grade system prompt looks like, and how MCP connects agents to business systems. Those posts describe the architecture of any AI agent. This one narrows the focus to a specific one: Claude Code.
Out of the box, Claude Code is a capable general-purpose coding agent. It reads your files, edits your code, runs your tests, and commits your changes. But it doesn’t know your team’s conventions. It doesn’t know that src/api/ files need input validation, or that every PR needs a changelog entry, or that it should never touch package-lock.json. It doesn’t have access to your Postgres staging database or your Sentry error feed.
The gap between “capable coding agent” and “productive member of your team” is configuration. Five of Claude Code’s extension points do most of the work of closing that gap: hooks, custom skills, MCP servers, plugins, and CLAUDE.md instruction files. Each solves a different problem. Together, they turn a generic agent into one that understands your codebase, respects your rules, and connects to your infrastructure.
Here’s how each one works, when to use it, and where they break.
The Five Extension Points
Before diving into each mechanism, it helps to see how they relate:
CLAUDE.md → What the agent knows (context, rules, conventions)
Skills → What the agent can be asked to do (custom commands)
MCP Servers → What systems the agent can talk to (external tools)
Hooks → What happens automatically around agent actions (event triggers)
Plugins → How you package and share all of the above
CLAUDE.md is passive — it shapes behavior through instructions. Skills are on-demand — you invoke them with a slash command, or Claude loads one when its description matches the task. MCP servers extend the agent’s toolset with external capabilities. Hooks are event-driven — they fire automatically when the agent does something. Plugins are the distribution format — a way to bundle any combination of the other four into an installable package.
Each one can be used independently. The real power is in composing them.
Hooks: Intercepting the Agent Loop
Hooks are shell commands that fire automatically at specific points in Claude Code’s execution cycle. If you’ve worked with Git hooks or CI/CD pipelines, the mental model is the same: define a trigger event, attach a handler, and it runs without anyone asking.
The difference from Git hooks is the breadth of events available. Claude Code exposes hooks for nearly every meaningful moment in its lifecycle:
| Event | When It Fires |
|---|---|
PreToolUse | Before a tool executes — can block it |
PostToolUse | After a tool succeeds |
UserPromptSubmit | When you press Enter, before Claude processes |
Notification | When Claude sends a notification |
SubagentStart / SubagentStop | When parallel agents spin up or finish |
Stop | When Claude finishes responding |
PreCompact | Before context window compaction |
That is a subset. The current hooks reference lists more than thirty events, including SessionStart, PermissionRequest, PostToolUseFailure, and PostCompact.
Hooks are configured in your settings files — either per-project in .claude/settings.json or globally in ~/.claude/settings.json. Each hook receives a JSON payload on stdin with context about the event: session ID, working directory, tool name, tool input, and more. The hook’s exit code determines what happens next.
Three exit codes matter:
- Exit 0 — success, continue normally. Claude Code applies any JSON control fields printed on stdout; plain-text stdout reaches Claude’s context only for a few events such as
UserPromptSubmitandSessionStart, and otherwise goes to the debug log. - Exit 2 — block the action (on events that can block). Stderr is fed back to Claude as the reason.
- Any other code — non-blocking error, execution continues. That includes exit 1, so a policy hook must use exit 2.
Example: Auto-Format After Every Edit
The most immediately useful hook is post-edit formatting. Every time Claude writes or modifies a file, Prettier (or your formatter of choice) runs automatically:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write 2>/dev/null; exit 0"
}
]
}
]
}
}
The matcher field filters by tool name. A value made only of letters, digits, _, -, spaces, commas, and | is an exact-match list; anything else is treated as an unanchored JavaScript regex. "Edit|Write" means this hook only fires when Claude uses the Edit or Write tools — not when it runs Bash commands or reads files.
Example: Blocking Dangerous Operations
This is where hooks become a governance mechanism. A PreToolUse hook on the Bash tool can inspect every command before it executes:
#!/bin/bash
# .claude/hooks/guard-bash.sh
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
BLOCKED_PATTERNS=("rm -rf" "drop table" "DROP TABLE" "truncate" "--force" "sudo")
for pattern in "${BLOCKED_PATTERNS[@]}"; do
if [[ "$COMMAND" == *"$pattern"* ]]; then
echo "Blocked: command contains '$pattern'" >&2
exit 2
fi
done
exit 0
Make the script executable (chmod +x), and reference it through ${CLAUDE_PROJECT_DIR} so it resolves no matter which directory Claude is working in. When this hook returns exit code 2, Claude doesn’t execute the command — and it receives the stderr message explaining why. It can then adjust its approach, which is the same self-correction loop we described in the tool-calling breakdown. The agent sees an error, reasons about it, and tries something different.
The corresponding settings entry:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"${CLAUDE_PROJECT_DIR}\"/.claude/hooks/guard-bash.sh"
}
]
}
]
}
}
Example: Protecting Critical Files
Some files should never be edited by an AI agent — environment configs, lock files, CI workflows. A file-protection hook catches these before the edit happens:
#!/bin/bash
# .claude/hooks/protect-files.sh
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
PROTECTED=(".env" "package-lock.json" ".github/workflows" "secrets")
for pattern in "${PROTECTED[@]}"; do
if [[ "$FILE_PATH" == *"$pattern"* ]]; then
echo "Blocked: $FILE_PATH is a protected file" >&2
exit 2
fi
done
exit 0
This is the “principle of least privilege” from our system prompt analysis — except instead of relying on the agent to follow instructions, you’re enforcing it at the infrastructure level. Instructions can be forgotten or overridden during long sessions. Hooks can’t.
Beyond Shell Commands
Hooks aren’t limited to shell scripts. Claude Code supports five handler types:
command— shell command (what we’ve seen above)http— POSTs the event JSON to a URL (for webhook integrations)mcp_tool— calls a tool on a configured MCP serverprompt— single-turn LLM evaluation (the hook itself uses AI to make a decision)agent— spins up a subagent that can use tools like Read, Grep, and Glob before deciding (marked experimental)
The prompt type is particularly interesting. You can write a hook that asks an LLM “does this code change look safe?” and blocks the action if the answer is no. It’s AI governing AI — expensive, but powerful for high-stakes workflows.
Custom Skills: Teaching the Agent New Tricks
Skills (formerly called “custom slash commands”) let you define reusable prompts that Claude Code can execute on demand. If hooks are automatic triggers, skills are on-demand ones — you invoke them by name, and Claude can also load a skill itself when its description matches the task. Add disable-model-invocation: true to keep a skill manual-only.
A skill is a markdown file with optional YAML frontmatter. Place it in .claude/skills/<skill-name>/SKILL.md for project scope or ~/.claude/skills/<skill-name>/SKILL.md for global scope. The directory name becomes the slash command.
Basic Example: A Code Review Skill
<!-- .claude/skills/review/SKILL.md -->
---
name: review
description: Review staged changes for quality and security
allowed-tools: Bash(git diff *)
disallowed-tools: Edit Write
---
Review the currently staged git changes (`git diff --cached`).
Check for:
1. Security issues (injection, hardcoded secrets, missing input validation)
2. Error handling gaps (unhandled promises, missing catch blocks)
3. Style violations against the project's conventions
4. Logic errors or edge cases
Format your findings as a numbered list with file paths and line numbers.
If everything looks good, say so briefly.
Now /review is available as a command in any Claude Code session within this project. The two tool fields do different jobs: allowed-tools pre-approves the listed tools for that turn so git diff runs without a permission prompt (it does not restrict anything), while disallowed-tools removes Edit and Write from Claude’s tool pool while the skill is active — the guardrail you want when the skill should analyze but not modify.
Dynamic Context With Shell Injection
Skills support a backtick syntax that runs shell commands before the prompt is sent to Claude. This lets you inject live context:
<!-- .claude/skills/fix-issue/SKILL.md -->
---
name: fix-issue
description: Fix a GitHub issue by number
argument-hint: <issue-number>
disable-model-invocation: true
allowed-tools: Bash(gh issue view *)
---
Here is the GitHub issue to fix:
!`gh issue view $ARGUMENTS --json title,body,labels,comments`
Fix this issue following our coding standards. Write tests for your changes.
Run the test suite before finishing.
$ARGUMENTS captures everything after the slash command — so /fix-issue 42 passes 42 to the gh issue view command. The issue data gets injected into the prompt before Claude sees it. The allowed-tools line matters here: injected commands never prompt for permission, so outside auto mode a command your permission rules don’t already allow aborts the whole skill.
Subagent Skills
For complex workflows, you can run a skill in an isolated context by forking it into a subagent:
---
name: audit-deps
description: Audit project dependencies for vulnerabilities
context: fork
agent: Explore
---
The context: fork directive runs the skill in a separate context window, keeping the main conversation clean. The agent field selects the subagent type — Explore is optimized for read-only codebase analysis.
Legacy Compatibility
If you have existing .claude/commands/<name>.md files, they still work and accept the same frontmatter except name and paths. What they can’t do is carry supporting files, since a command is a single file rather than a skill directory. Migrate a command to .claude/skills/<name>/SKILL.md when it needs reference docs or scripts alongside it.
MCP Servers: Connecting to External Systems
We covered how MCP works at the protocol level — the client-server architecture, JSON-RPC transport, and how to build a server from scratch. Here, the focus is narrower: how to configure MCP servers inside Claude Code so the agent can talk to your infrastructure.
Adding Servers
The fastest path is the CLI:
# Remote server over HTTP
claude mcp add --transport http github https://api.githubcopilot.com/mcp/
# Local server running as a subprocess
claude mcp add --transport stdio postgres -- \
npx -y @bytebase/dbhub --dsn "postgresql://readonly:pass@localhost:5432/mydb"
# With environment variables for authentication
claude mcp add --env AIRTABLE_API_KEY=YOUR_KEY --transport stdio airtable -- \
npx -y airtable-mcp-server
Each server gets registered and its tools become available to Claude Code automatically. MCP tools follow a namespacing convention — mcp__<server>__<tool> — so mcp__postgres__query or mcp__github__search_repositories.
Scoping
Servers can be scoped to control visibility:
--scope local(default) — stored in~/.claude.json, only you, only this project--scope project— stored in.mcp.jsonat project root, shared via version control--scope user— stored in~/.claude.json, available across all your projects
For team-shared servers, project scope is what you want. The .mcp.json file goes into your repo:
{
"mcpServers": {
"staging-db": {
"command": "npx",
"args": ["-y", "@bytebase/dbhub", "--dsn", "${DATABASE_URL}"],
"env": {}
}
}
}
The ${DATABASE_URL} syntax pulls from environment variables at runtime — so credentials stay out of version control while the server configuration is shared.
Claude Code as an MCP Server
An interesting capability that flips the relationship: claude mcp serve exposes Claude Code itself as an MCP server. This means other MCP-compatible tools — Claude Desktop, custom agents, IDE plugins — can use Claude Code’s file editing and analysis capabilities through the standard protocol.
The practical use case: orchestrating Claude Code from a higher-level automation system. A CI pipeline that uses MCP to ask Claude Code to review a PR, for instance, or a monitoring agent that delegates code fixes to Claude Code when it detects a regression.
When MCP Gets Heavy
As you add servers, the tool count grows. Claude Code handles this with tool search, which is on by default on current Claude models: only tool names and server instructions load at session start, and Claude searches for a tool’s full definition before calling it. This keeps context usage manageable, but it adds a step to tool selection. Setting ENABLE_TOOL_SEARCH=auto switches to threshold mode, which loads tools upfront until their definitions reach 10% of the context window, and ENABLE_TOOL_SEARCH=false loads everything upfront.
The mitigation is the same advice from our MCP deep dive: keep servers narrow. One server per domain, with the minimum viable set of tools. Three servers with five tools each outperform one server with fifteen.
Plugins: Packaging It All Together
Hooks, skills, MCP configs, and settings are useful individually. Plugins let you bundle them into a single installable package — useful for sharing setups across projects or distributing them to a team.
A plugin is a directory with a .claude-plugin/ manifest:
my-workflow-plugin/
.claude-plugin/
plugin.json
commands/ # Legacy slash commands
agents/ # Subagent definitions
skills/ # Skill definitions
review/
SKILL.md
hooks/
hooks.json # Event handlers
scripts/
format.sh
guard.sh
.mcp.json # MCP server configurations
settings.json # Defaults (only agent and subagentStatusLine apply)
The plugin.json manifest is minimal (only name is required, and the manifest itself is optional):
{
"name": "my-workflow-plugin",
"description": "Team development workflow — formatting, guards, and DB access",
"version": "1.0.0",
"author": {
"name": "Your Team"
}
}
Using Plugins
For local development and testing:
claude --plugin-dir ./my-workflow-plugin
Multiple plugins can be loaded simultaneously:
claude --plugin-dir ./security-plugin --plugin-dir ./formatting-plugin
Plugin-Scoped Isolation
Plugin skills are namespaced to avoid collisions. A skill named review in a plugin called my-workflow becomes /my-workflow:review (plain /review also works when nothing else claims the name). Plugin hooks run alongside project hooks — all matching hooks fire in parallel. Identical handlers defined in more than one settings file run once, but a plugin’s copy of the same handler runs separately.
Plugin MCP servers start automatically at session startup when the plugin is enabled, and connect or disconnect when you enable or disable the plugin mid-session. The ${CLAUDE_PLUGIN_ROOT} variable resolves to the plugin’s directory, so server commands and scripts can use relative paths without hardcoding:
{
"mcpServers": {
"staging-db": {
"command": "${CLAUDE_PLUGIN_ROOT}/servers/start-db.sh",
"args": ["--config", "${CLAUDE_PLUGIN_ROOT}/config.json"]
}
}
}
When to Use Plugins vs. Direct Configuration
For a single project with a stable team, direct configuration in .claude/settings.json, .claude/skills/, and .mcp.json is simpler. Plugins add value when:
- You’re sharing the same setup across multiple repositories
- You’re distributing a workflow to people outside your team
- You want to version and release your configuration independently from your codebase
- You need to combine hooks, skills, and MCP servers that logically belong together
Don’t reach for plugins if .claude/settings.json and a few skill files get the job done. Premature packaging is the same anti-pattern as premature abstraction.
CLAUDE.md: The Governance Layer
Every extension point we’ve covered so far adds capability — hooks enforce rules, skills add commands, MCP servers add tools. CLAUDE.md is different. It shapes how the agent thinks about your project.
A CLAUDE.md file at your project root is automatically loaded into every Claude Code session. It survives context compaction (the agent re-reads it from disk when the context window is compressed), which makes it the most durable way to communicate project conventions.
What Belongs in CLAUDE.md
The most effective CLAUDE.md files we’ve seen share a pattern: they’re short, specific, and focused on preventing mistakes the agent has actually made.
# Project Conventions
- Use Bun, not npm. Run `bun test` before committing.
- API handlers live in `src/api/handlers/`. Each handler is one file.
- All database queries go through `src/db/queries.ts` — never write raw SQL in handlers.
- Error responses use the `ApiError` class from `src/lib/errors.ts`.
- Do not modify files in `src/generated/` — they are auto-generated from the OpenAPI spec.
# Testing
- Unit tests live next to source files: `foo.ts` → `foo.test.ts`
- Integration tests live in `tests/integration/`
- Run `bun test` for unit tests, `bun test:integration` for integration tests
# Git
- Commit messages follow conventional commits: feat:, fix:, refactor:
- Never amend commits that have been pushed
- Always create a new branch for changes: `<type>/<short-description>`
Notice the pattern: every line answers the question “would removing this cause Claude to make a mistake?” This aligns with the anti-pattern-driven approach from our system prompt analysis — specific failure modes are more instructive than general advice.
Scoping and Hierarchy
CLAUDE.md files cascade through your project structure:
| Scope | Location | Loaded |
|---|---|---|
| Organization (managed) | /etc/claude-code/CLAUDE.md | Always, cannot be excluded |
| Project (shared) | ./CLAUDE.md or ./.claude/CLAUDE.md | At session start |
| User (personal) | ~/.claude/CLAUDE.md | At session start |
| Local (personal, one project) | ./CLAUDE.local.md | At session start |
| Subdirectory | src/api/CLAUDE.md | On demand, when Claude reads files in that directory |
Subdirectory CLAUDE.md files are powerful for monorepos — different rules for different parts of the codebase, loaded only when relevant. A src/api/CLAUDE.md with API-specific conventions won’t consume context when Claude is working on the frontend.
The Import System
CLAUDE.md files can reference other files in your project:
See @README.md for project overview
See @docs/api-spec.md for endpoint documentation
See @package.json for available scripts
The @ syntax pulls the referenced file’s content into the instruction set. This keeps CLAUDE.md lean while giving the agent access to documentation you’re already maintaining.
Rules for Fine-Grained Control
For conventions that apply only to specific file patterns, the .claude/rules/ directory offers path-scoped rules:
# .claude/rules/api-handlers.md
---
paths:
- "src/api/**/*.ts"
---
# API Handler Rules
- Every handler must validate input using zod schemas
- Return types must use the `ApiResponse<T>` wrapper
- Include rate limiting middleware for all public endpoints
- Log all errors through the structured logger, never console.log
These rules load only when Claude accesses files matching the glob pattern. For a large codebase with distinct conventions across domains, this is far more efficient than cramming everything into a single CLAUDE.md.
Composing the Pieces: A Realistic Setup
Here’s how these extension points work together in a production development workflow. This is representative of what we configure for our own projects — not a theoretical ideal, but a setup that’s survived daily use.
CLAUDE.md — project conventions, key file paths, testing commands, commit format. Under 50 lines. This is the governance layer — the equivalent of the system prompt’s “task execution philosophy” section, applied to your specific project.
Two skills:
/review— reviews staged changes against the project’s standards/fix-issue <number>— pulls a GitHub issue and implements a fix
Two hooks:
PostToolUseonEdit|Write— runs the project’s formatterPreToolUseonBash— blocks destructive commands and protected file patterns
One MCP server:
- Staging database (read-only) — lets Claude query real data while investigating bugs
No plugins — everything lives directly in .claude/ and .mcp.json. It’s one project with one team. Plugins would add packaging overhead with no benefit.
The configuration totals about 80 lines of JSON and 30 lines of markdown across five files. That’s it. The agent handles the rest — reading code, understanding context, making decisions — using the general capabilities it already has.
Where This Breaks
These extension points solve real problems, but they introduce their own failure modes.
Hook performance degrades with scale. Every matching hook runs on every matching event. A PostToolUse hook that runs Prettier on every file edit is fine. Five PostToolUse hooks that each run a different linter will noticeably slow down the agent. All matching hooks run in parallel, which helps, but the total latency of the slowest hook still adds up across hundreds of tool calls in a long session.
CLAUDE.md has a soft capacity limit. The official guidance is to target under 200 lines per CLAUDE.md file, because longer files consume more context and reduce adherence. Every rule you add competes with the others for the model’s attention, so a bloated file makes all of its instructions less reliable, not just the new one. This is the same context window pressure we described in the tool-calling article: every token of instruction competes with every token of code and conversation.
MCP servers are a trust boundary. When you connect an MCP server, you’re giving the agent access to whatever that server exposes. A read-only database connection is low-risk. A server with write access to production data is a different story. The same prompt injection risks we discussed for MCP in general apply here — tool results can contain adversarial content, and the agent will process them as context.
Hook changes apply live, mistakes included. Direct edits to hooks in settings files are normally picked up by Claude Code’s file watcher mid-session, and /hooks opens a read-only browser of what is currently configured. That makes iteration fast, but a half-finished hook script starts firing as soon as you save it, so test hook scripts by piping sample JSON into them before wiring them up.
Plugin ecosystem maturity varies. Community plugins range from battle-tested to abandoned prototypes. Vet any third-party plugin the same way you’d vet a dependency — check the source, review the hooks and MCP configs, and understand what permissions it’s requesting.
The Compound Effect
None of these extension points is revolutionary on its own. A CLAUDE.md file is just a text file. A hook is just a shell script. A skill is just a markdown template. An MCP server is just a JSON-RPC endpoint.
The value is in the composition. CLAUDE.md gives the agent judgment about your project. Skills give it reusable workflows. MCP servers give it access to your infrastructure. Hooks give it guardrails. Together, they transform a general-purpose coding agent into one that operates within the specific constraints and conventions of your team — which is exactly the gap that prevents most AI agent pilots from reaching production.
The underlying pattern is the same one that makes any agent production-ready: capability without governance is a liability. These five extension points give you both — and the ability to tune the balance between autonomy and control as your trust in the system grows. A sixth has since added time and events as triggers: Claude Code Routines run the same workflows on a schedule, from an API call, or from an external event, with no one at the keyboard.
We build and configure AI agent workflows for teams that need them to work in production, not just in demos. If you’re evaluating how AI coding agents fit into your development process — or trying to get more value from tools you’re already using — get in touch.