DEV Community

Cover image for MCP, Subagents, and Hooks in Claude Code: The Guide I Wish I'd Had
Alvarito1983
Alvarito1983

Posted on

MCP, Subagents, and Hooks in Claude Code: The Guide I Wish I'd Had

Originally published on El Rack — Spanish tech reviews from a sysadmin/homelab perspective.

Claude Code is capable right out of the box, but it starts to feel thin the moment your project needs to touch external systems: a ticket tracker, a database, your own VPS. This guide covers the four pieces that turn it into a real production tool:

  • MCP (/mcp): connecting external services as tools
  • Subagents: delegating tasks to isolated instances with their own context
  • Skills / slash commands: packaging repetitive prompts as commands
  • Hooks: deterministic guardrails that don't depend on the AI "remembering" an instruction

You'll need Claude Code installed and authenticated, and a terminal open in any project folder.

1. Connect your first MCP server

MCP (Model Context Protocol) lets Claude Code use tools it doesn't ship with by default. Those tools live in MCP servers: local processes or hosted services reachable over a URL. You add them with claude mcp add, no hand-editing JSON required. Start with the official docs server — it needs no account and no config:

claude mcp add --transport http claude-code-docs https://code.claude.com/docs/mcp
Enter fullscreen mode Exit fullscreen mode

Confirm it connected:

claude mcp list
Enter fullscreen mode Exit fullscreen mode

You should see ✔ Connected. From inside a session, manage servers any time with /mcp.

2. Local (stdio) servers and OAuth-gated servers

A stdio server is a program Claude Code launches as a subprocess — useful when it needs access to your filesystem or a browser. Example with Playwright, which requires no account:

claude mcp add playwright -- npx -y @playwright/mcp@latest
Enter fullscreen mode Exit fullscreen mode

The -- separates Claude Code's own flags from the command that starts the server. For services that require sign-in (Sentry, Linear, Notion, GitHub), you add them the same way and authenticate from inside the session:

claude mcp add --transport http sentry https://mcp.sentry.dev/mcp
Enter fullscreen mode Exit fullscreen mode

After adding, you'll see ! Needs authentication. Start a session, run /mcp, select the server, and choose "Authenticate" — your browser opens for sign-in.

If the service uses a static token instead of OAuth (common with self-hosted instances), pass it directly:

claude mcp add --transport http my-server http://my-host:3001/api/mcp --header "Authorization: Bearer YOUR_TOKEN"
Enter fullscreen mode Exit fullscreen mode

3. Pick the right scope: local, project, or user

By default, every server is registered at "local" scope: private to you, active only in the current project. Two alternatives depending on how widely you want to share it:

# Available in all your projects, still private
claude mcp add --scope user --transport http claude-code-docs https://code.claude.com/docs/mcp

# Shared with the team via the repo (writes .mcp.json)
claude mcp add --scope project --transport http claude-code-docs https://code.claude.com/docs/mcp
Enter fullscreen mode Exit fullscreen mode
Scope File Available to
local ~/.claude.json (project entry) Only you, only this project
project .mcp.json at the repo root Everyone who clones the repo
user ~/.claude.json (top-level mcpServers) Only you, all your projects

If you'd rather write .mcp.json by hand to keep it version-controlled with the team:

{
  "mcpServers": {
    "claude-code-docs": {
      "type": "http",
      "url": "https://code.claude.com/docs/mcp"
    },
    "playwright": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@playwright/mcp@latest"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

4. Create your first custom subagent

A subagent is an instance with its own context, its own system prompt, and its own tools, working in isolation and returning only a summary. It's the fix for two problems: flooding your main conversation with logs or results you won't reuse, and re-spawning the same kind of worker with the same instructions over and over.

Claude Code ships with three built-in subagents: Explore (read-only code search), Plan (research during plan mode), and general-purpose (complex tasks with access to everything). For a custom one, just ask Claude to write it:

Create a personal subagent in ~/.claude/agents/ called "code-reviewer" that
reviews code for quality, security, and best practices. Make it read-only
and have it use Sonnet.
Enter fullscreen mode Exit fullscreen mode

Claude writes the file with YAML frontmatter plus the system prompt:

---
name: code-reviewer
description: Reviews code for quality, security, and best practices. Use after writing or modifying code.
tools: Read, Grep, Glob
model: sonnet
---

You are a senior code reviewer. For each issue you find, explain the
problem, show the current code, and provide an improved version.
Enter fullscreen mode Exit fullscreen mode

The most useful frontmatter fields: tools (allowlist), disallowedTools (denylist), model (sonnet/opus/haiku/inherit), and mcpServers (gives an MCP server to that subagent alone, without loading its context into the main conversation).

Save it in .claude/agents/ to scope it to this project, or ~/.claude/agents/ to make it available everywhere.

5. Invoking subagents: natural language, @-mention, or a full session

There are three ways to use a subagent, from least to most explicit:

# Natural language: Claude decides whether to delegate
Use the code-reviewer subagent to review my recent changes

# @-mention: forces that specific subagent to run
@"code-reviewer (agent)" review the authentication logic

# Run the whole session as that subagent (its system prompt and tools)
claude --agent code-reviewer
Enter fullscreen mode Exit fullscreen mode

For independent investigations, you can request several subagents in parallel:

Research the authentication, database, and API modules in parallel using
separate subagents
Enter fullscreen mode Exit fullscreen mode

Each one explores its own area in isolation; you only get the synthesized summary back in your main conversation.

6. Package repetitive prompts: slash commands and skills

If you type the same instruction over and over, turn it into a command. Classic commands (.claude/commands/*.md) still work, but the recommended approach now is Skills (.claude/skills/<name>/SKILL.md) — if a command and a skill share a name, the skill wins.

Classic command, saved as .claude/commands/audit-disk.md:

---
description: Disk space audit with a configurable threshold
allowed-tools: Read, Bash, Grep
argument-hint: [threshold-percentage]
---

Audit disk space across the servers. Alert threshold: $ARGUMENTS%.
Enter fullscreen mode Exit fullscreen mode

Invoke it with /audit-disk 85. $ARGUMENTS captures everything typed after the command; !`command` injects live shell output (e.g. !`git diff --cached`).

Same idea as a skill, at .claude/skills/audit-disk/SKILL.md:

---
name: audit-disk
description: Disk space audit with a configurable threshold. Use when the user asks to check disk space on the servers.
allowed-tools: Read, Bash, Grep
---

Audit disk space across the servers...
Enter fullscreen mode Exit fullscreen mode

The advantage of skills: they can bundle several reference files in the same folder, and Claude can invoke them on its own, without you typing the slash.

7. Put deterministic guardrails in place with hooks

If you run in an autonomous mode (--dangerously-skip-permissions), hooks are how you block dangerous operations without relying on the AI remembering a rule. They fire every time, at the exact lifecycle point you define.

The two most-used events: PreToolUse (before a tool runs — good for blocking) and PostToolUse (after — good for formatting, linting, or logging). Configure them in settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "./scripts/validate-command.sh" }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

The script receives the command as JSON via stdin, and exit code 2 blocks the operation. Example that stops an overly broad pkill on a server where multiple processes share the same name:

#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')

if echo "$COMMAND" | grep -qE 'pkill.*node|rm -rf /'; then
  echo "Blocked: command too broad or destructive. Use an exact PID." >&2
  exit 2
fi

exit 0
Enter fullscreen mode Exit fullscreen mode

Make it executable:

chmod +x ./scripts/validate-command.sh
Enter fullscreen mode Exit fullscreen mode

Another common one: auto-format every file Claude touches, without having to ask:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          { "type": "command", "command": "npx prettier --write \"$CLAUDE_TOOL_INPUT_FILE_PATH\"" }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Wrapping up

Put these four pieces together and Claude Code stops being a terminal assistant and becomes a real automation layer: it connects to whatever it needs (MCP), delegates whatever would clutter its context (subagents), packages whatever repeats (skills), and respects hard limits that don't depend on its memory (hooks). Suggested adoption order: start with one low-risk MCP server, add one read-only subagent, migrate your commands to skills whenever you have time, and — the highest-leverage one if you run in autonomous mode — add at least one hook that blocks the operation you're most afraid of running by accident.


Tutorial by Álvaro Fraguas Bravo for El Rack.

Top comments (0)