Claude · 12 · Part 6

Building Your Own Agentic Development Environment

Chapter 10 introduced the features; this page assembles them into an environment a team can share: custom skills, an MCP server you write yourself, hooks and automation, external tools, project instructions, a plugin to package it all, and the permission and enterprise controls that keep it safe. The MCP server is real code, tested by speaking the protocol to it; the plugin and managed-settings files were validated; the Claude Code wiring is described from the documentation.

1
MCP server written
4
Protocol messages tested
4
Managed delivery routes
Information current as of: 2026-10-30 — MCP protocol revision 2025-11-25 (the latest dated revision found in the specification repository); Claude Code 2.1.314.
Executed vs documented. Executed on this machine: the MCP server's protocol exchange, jq/YAML validation of the plugin manifest, MCP config, managed-settings file and skill frontmatter. Not executed: connecting the server to Claude Code (that would change your Claude configuration), installing the plugin, deploying managed settings, or any live session. So "Claude Code will call this tool" is documented behaviour, not something observed here.
01
The Stack

Layers of an Agentic Environment

Treat the environment as layers, from what the whole organisation fixes to what one developer adds. Each layer has a place it lives and a person who owns it.

Who owns which layer, and where it lives
Organisation · managed settings (cannot be overridden)deny rules · login method · allowed providers · bypass off Plugins · versioned bundles from a marketplaceskills · agents · hooks · MCP servers · workflows Project · committed in the repositoryCLAUDE.md · settings.json · skills · .mcp.json You · ~/.claude and local filespersonal skills · CLAUDE.local.md · settings.local.json The sessioncontext · tools · permissions at runtime

Anthropic's documentation describes how these combine: CLAUDE.md files are additive; skills and subagents override by name (managed over user over project for skills); MCP servers override by name (local over project over user); hooks merge so every registered hook fires; and managed settings apply above every other level (Source 1, Source 2). Design with that in mind: put non-negotiable rules at the managed layer, shared conventions in the project, and personal taste in your own files.

02
Part 1

Custom Skills, Beyond the Basics

Chapter 10 showed a skill with a pre-approved tool and a ! command. The frontmatter has more levers (Source 3):

FieldWhat it buys you
context: fork with agentRuns the skill in a subagent, so its reads stay out of your conversation
disable-model-invocation: trueOnly you can start it — right for anything with side effects
user-invocable: falseHidden from the / menu; Claude can still use it as background knowledge
pathsGlob patterns limiting when Claude loads it automatically
argument-hint, $ARGUMENTSTakes input: /explain-endpoint GET /api/books/{id}
hooksRegisters hooks only while the skill is active
.claude/skills/explain-endpoint/SKILL.md — bookshelf
--- name: explain-endpoint description: Explains how one REST endpoint of bookshelf works, from controller to database, without changing any file. Use when asked how a given endpoint behaves. argument-hint: "[METHOD /path]" context: fork agent: Explore --- Explain how `$ARGUMENTS` is handled in this project. 1. Find the controller method that maps it. 2. Follow the call into the service and repository layers. 3. Name every class and the line where validation, mapping and persistence happen. 4. Report the SQL statement count if a test already measures it. Read only. Do not edit files.
Actual output · YAML frontmatter parsed with PyYAML; keys compared with the documented field table · executed 2026-10-30
frontmatter parses; keys: ['name', 'description', 'argument-hint', 'context', 'agent'] unknown keys vs documented table: none

What that does and does not prove: the file is syntactically valid and uses only documented fields. It does not prove Claude selects it appropriately. Descriptions drive automatic loading, so write them with the words a request would contain; and keep the count of skills manageable — names always load, and when there are many, some lose their descriptions (Source 4).

03
Part 2

Write Your Own MCP Server

A skill teaches Claude how to use something; an MCP server gives it a tool. The protocol is smaller than it looks. In the stdio transport the client launches your server as a subprocess; messages are single-line JSON-RPC 2.0 objects on standard input and output; and the server may log to standard error but must write nothing else to standard output (Source 5).

The handshake, then use
Client Your server initialize (protocolVersion, capabilities) result: version, capabilities, serverInfo notifications/initialized (no reply) tools/list tools: name, description, inputSchema tools/call (name, arguments) content[…], isError

The spec requires the client to open with initialize (protocol version, capabilities, client info), the server to answer with its own, and the client to follow with a notifications/initialized notification before normal operation. Tools are discovered with tools/list and invoked with tools/call; a result carries content blocks and an isError flag. The spec also says tool execution errors belong in the result with isError: true so a model can self-correct, while unknown tools and malformed requests are JSON-RPC protocol errors (Source 5). Here is a complete server, about 40 lines, with no SDK, exposing one read-only tool backed by Chapter 11's feature_list.json:

mcp/feature-server.mjs
#!/usr/bin/env node // Minimal MCP server over stdio (newline-delimited JSON-RPC 2.0). No SDK. // Exposes one tool, feature_status, backed by feature_list.json. import { readFileSync } from 'node:fs'; import { createInterface } from 'node:readline'; const PROTOCOL = '2025-11-25'; const FILE = process.env.FEATURE_FILE ?? 'feature_list.json'; const tools = [{ name: 'feature_status', description: 'Report how many bookshelf features pass and which one is next.', inputSchema: { type: 'object', properties: {}, additionalProperties: false }, }]; function send(msg) { process.stdout.write(JSON.stringify(msg) + '\n'); } function ok(id, result) { send({ jsonrpc: '2.0', id, result }); } function fail(id, code, message) { send({ jsonrpc: '2.0', id, error: { code, message } }); } const rl = createInterface({ input: process.stdin }); rl.on('line', (line) => { if (!line.trim()) return; let msg; try { msg = JSON.parse(line); } catch { return fail(null, -32700, 'Parse error'); } if (msg.id === undefined) return; // notifications need no reply switch (msg.method) { case 'initialize': return ok(msg.id, { protocolVersion: PROTOCOL, capabilities: { tools: {} }, serverInfo: { name: 'bookshelf-features', version: '0.1.0' }, }); case 'tools/list': return ok(msg.id, { tools }); case 'tools/call': { if (msg.params?.name !== 'feature_status') return fail(msg.id, -32602, 'Unknown tool: ' + msg.params?.name); const list = JSON.parse(readFileSync(FILE, 'utf8')).features; const passing = list.filter((f) => f.status === 'passing').length; const next = list.find((f) => f.status !== 'passing'); const text = `${passing}/${list.length} passing; next: ${next ? next.name : 'none'}`; return ok(msg.id, { content: [{ type: 'text', text }], isError: false }); } default: return fail(msg.id, -32601, 'Method not found: ' + msg.method); } });

You can test a server without any client by piping the protocol into it. This is the same exchange as the diagram, followed by a call to a tool that does not exist:

Prerequisites: Node.js (v24.15.0 used), jq. Run from the project root.
$ printf '%s\n' \ '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"manual-test","version":"0"}}}' \ '{"jsonrpc":"2.0","method":"notifications/initialized"}' \ '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \ '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"feature_status","arguments":{}}}' \ '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"drop_database","arguments":{}}}' \ | node mcp/feature-server.mjs | jq -c .
Actual output · executed 2026-10-30 · one response per request; the notification gets none
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","capabilities":{"tools":{}},"serverInfo":{"name":"bookshelf-features","version":"0.1.0"}}} {"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"feature_status","description":"Report how many bookshelf features pass and which one is next.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}}]}} {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"5/7 passing; next: pagination for list"}],"isError":false}} {"jsonrpc":"2.0","id":4,"error":{"code":-32602,"message":"Unknown tool: drop_database"}}

What it proves: the server follows the message shapes in the specification, including the -32602 "Unknown tool" error the spec itself shows as its example. What it does not prove: that a given Claude Code version can connect to it — I did not register it, because claude mcp add writes to your configuration. To register it for a project, the docs give this shape (stdio servers need no url):

.mcp.json (project scope, checked in) — validated as JSON, not connected
{ "mcpServers": { "bookshelf-features": { "type": "stdio", "command": "node", "args": ["mcp/feature-server.mjs"] } } }
Security of what you build. Claude Code asks you to approve project-scoped servers from .mcp.json, and approvals committed to a repository do not count until you trust the workspace (Source 6). A local stdio server runs outside Claude Code's Bash sandbox, with your privileges (Source 7). Keep a tool read-only unless writing is the point, validate every argument, and treat what comes back from other servers as untrusted input. The MCP specification itself says there should always be a human in the loop with the ability to deny tool invocations (Source 5).
04
Part 3

Hooks and Automation

A hook is the right place for anything that must happen without the model's cooperation. Beyond the two in Chapter 10, Anthropic's docs and best-practices page show these patterns (Source 8, Source 9):

GoalEventShape
Format every file Claude editsPostToolUse, matcher Edit|WriteA command that reads tool_input.file_path from the JSON on stdin and runs your formatter
Block edits to a protected folderPreToolUseExit code 2 when the path is under that folder
Refuse to finish on a failing buildStop./mvnw -B -q test 1>&2 || exit 2
Give a new session its bearingsSessionStartText the hook prints to stdout is added to Claude's context
Quality gates for teammatesTeammateIdle, TaskCreated, TaskCompletedExit 2 to keep a teammate working or to block task creation/completion
Test hooks like programs, because they are. Chapter 10 ran a hook script with three JSON inputs and checked the exit codes. Do the same for every hook you add, including the inputs you expect to pass: a hook that wrongly blocks ./mvnw will make the agent unusable.
  • Prefer deterministic handlers (command, http) for enforcement. prompt and agent hooks ask another model to decide and are not deterministic; the agent type is experimental. Add onFailure: "block" when the action should also be stopped if the hook itself crashes or times out (Source 9).
  • Hooks run with your privileges and outside the sandbox. Review hook scripts in anything you did not write.
05
Part 4

External Tool Integration

You need…PreferWhy
GitHub, AWS, cloud CLIsThe CLI (gh, aws)No per-tool listing in context; Claude can run --help on an unfamiliar one (Source 9, Source 10)
A system with no CLI (tracker, dashboard, internal API)An MCP serverPurpose-built tools; connection and auth handled by the server
Your own program that drives ClaudeThe Agent SDK (claude -p, Python, TypeScript)Same tools and agent loop, with orchestration under your control
Typed-language navigationA code-intelligence pluginDefinitions, references and type errors without scanning files

One cost note that shapes the choice: connected MCP servers add their tool names and instructions to context even when idle (full schemas are deferred), so disable servers you are not using via /mcp (Source 10).

06
Part 5

Project Instructions and Reusable Packaging

When the same setup is needed in several repositories, or you want versioned releases and updates, package it as a plugin: a directory of components that Claude Code installs and loads as one unit, with a manifest at .claude-plugin/plugin.json giving it a name (Source 11). A plugin can hold skills, agents, hooks, MCP servers and more. Here is bookshelf's, assembled from files already on this page and validated:

Actual output · executed 2026-10-30
$ find . -type f | sort ./agents/sql-reviewer.md ./.claude-plugin/plugin.json ./.mcp.json ./skills/verify-build/SKILL.md $ jq empty on every JSON file valid: .claude-plugin/plugin.json valid: .mcp.json valid: ../managed-settings.json
.claude-plugin/plugin.json
{ "name": "bookshelf-kit", "version": "0.1.0", "description": "Verification skill and SQL reviewer for the bookshelf team" }
  • Namespacing: plugin skills run as /plugin-name:skill-name, so they cannot collide with project skills (Source 11).
  • Scopes: install at user scope (you, every project), project scope (everyone in the repo, through committed .claude/settings.json; each person still installs it) or local scope. Cloud sessions do not load local plugins.
  • Cost: an enabled plugin is part of every session: the names and descriptions of its model-invocable skills and agents sit in context each turn, and its MCP servers and hooks run alongside. Check the footprint before installing; the /plugin interface shows a context-cost estimate for official-marketplace plugins.
  • Trust: what a plugin runs, it runs as you. Plugins from any marketplace can run code with your privileges, so review them first.
  • Not included on purpose: hooks. The plugin docs say plugins can carry hooks, but the path conventions for a plugin's hook scripts were not verified for this page, so the example omits them rather than guess.

Skills in repositories can also be run in CI: check out the repository, then pass the skill as the action's prompt (Chapter 11). Publishing and marketplaces are covered in Anthropic's plugin docs; hosting your own marketplace for a team is a marketplace.json catalogue you control.

07
Part 6

Agent Permissions and Isolation

Autonomy needs a boundary the model cannot argue with. Anthropic's docs offer three, and they stack (Source 12, Source 7, Source 13):

What each boundary covers
Dev container / VM — the whole Claude Code processcovers hooks, local MCP servers, plugins, helpers Permission rules — every tool calldeny → ask → allow; enforced by Claude Code, not the model Bash sandbox — shell commands and their child processesOS-enforced: filesystem writes + network via a hostname-checking proxynot covered: Read/Edit/Write/WebFetch tools, hooks, local MCP, excluded commands, unsandboxed retries · off by default
  • Permission rules (deny → ask → allow, first match wins) are enforced by Claude Code, not the model, and are shareable in .claude/settings.json.
  • The sandbox is an OS-enforced boundary around Bash (and PowerShell and Monitor) commands and their subprocesses: macOS uses Seatbelt, Linux and WSL2 use bubblewrap with a proxy relay; native Windows is not sandboxed. It is off by default (/sandbox or "sandbox": {"enabled": true}). Two gaps matter: reads are allowed across most of the machine by default, including credential files such as ~/.ssh and ~/.aws/credentials, until you restrict them; and failed commands can be retried unsandboxed unless allowUnsandboxedCommands is false.
  • A container or VM puts the whole process behind one boundary. The dev-container page's own warning: with --dangerously-skip-permissions, a container does not stop a malicious project from exfiltrating anything accessible inside it, including Claude Code credentials in ~/.claude; use it only with trusted repositories and avoid mounting host secrets such as ~/.ssh.
08
Part 7

Enterprise Considerations

Managed settings are what an organisation deploys to every machine; Claude Code applies them above every other level, so no user, project or local value overrides them, apart from a few security-sensitive exceptions where a stricter lower value still counts (Source 14).

Delivery routeHowWhen read
Server-managed settingsclaude.ai admin console (or a self-hosted Claude apps gateway)Fetched at startup, polled hourly
MDM / OS policymacOS configuration profile or Windows HKLM registry value (Jamf, Intune, Group Policy…)At startup; checked every 30 minutes
File-basedmanaged-settings.json in a system directoryAt startup; reloaded on change
Windows HKCUUser-level registry valueUsed only when no admin document is present above it

File locations: /Library/Application Support/ClaudeCode/managed-settings.json (macOS), /etc/claude-code/managed-settings.json (Linux and WSL) and C:\Program Files\ClaudeCode\managed-settings.json (Windows). By default Claude Code uses one managed source and ignores the others; the page documents the order and an opt-in to combine them. Cloud sessions in Anthropic-hosted environments do not read a device's MDM profile or file, so policy for them must come from server-managed settings.

managed-settings.json — a small policy using keys documented on the pages cited; validated as JSON only
{ "permissions": { "disableBypassPermissionsMode": "disable", "deny": [ "Read(./.env)", "Bash(git push *)" ] }, "forceLoginMethod": "claudeai", "allowedProviders": ["anthropic", "bedrock"], "disableWorkflows": false }
  • permissions.disableBypassPermissionsMode: "disable" stops engineers using --dangerously-skip-permissions at all.
  • forceLoginMethod and forceLoginOrgUUID restrict which accounts can sign in; allowedProviders lists which services a managed machine may reach Claude through (the Anthropic API, Amazon Bedrock, an LLM gateway…). With these set, an ANTHROPIC_API_KEY environment credential is blocked at startup, because organisation membership cannot be verified for it (Source 15).
  • Policy a repository file can quietly change is not policy. The dev-container page warns that policy placed in a repository Dockerfile can be removed by anyone with write access; for policy engineers cannot bypass, use server-managed settings or MDM.
  • Cost and observability: OpenTelemetry export gives per-user metrics on every setup; Teams and Enterprise have spend limits at organisation, group or member level; Console organisations use workspace limits (Source 16).
  • Data handling: telemetry and error reporting can be disabled individually (DISABLE_TELEMETRY, DISABLE_ERROR_REPORTING) or together with CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC (Source 17) — covered in Chapter 13.
  • Plugins at scale: managed settings can allowlist or block marketplaces, force-install plugins and turn off session-only loading.
09
Putting It Together

The bookshelf Environment, End to End

LayerWhat bookshelf hasStatus
InstructionsCLAUDE.md (commands, conventions, gotchas)Written (Ch 10)
GuardrailsDeny rules; PreToolUse hook (3 tests run); Stop hook; scripts/verify.sh (budget gate run)Hook and gate executed
Procedures/verify-build, /explain-endpoint skillsWritten; frontmatter validated
Reviewsql-reviewer subagentWritten
ToolsOwn MCP server (bookshelf-features)Protocol exchange executed
AutomationGitHub Actions test + review workflowParsed, not run
Packagingbookshelf-kit plugin (skill, agent, MCP)Manifest and JSON validated
Organisation policymanaged-settings.json sketchJSON validated; not deployed

Read the last column as the honest summary of this chapter. The environment is a set of small, inspectable files; the parts that could be exercised without a live Claude session were; the parts that depend on Claude Code's runtime behaviour are as documented, and the first thing to do when adopting any of it is to try it in a throwaway repository and watch it work.

10
Common Mistakes

Building the Environment

1 · Everything at once. Add a feature when its trigger fires (Chapter 10's map). A big up-front setup adds context cost and maintenance before it has paid for itself.
2 · Untested guardrails. Run each hook with inputs that should pass and inputs that should block. A wrongly blocking hook is as damaging as a missing one.
3 · An MCP tool that can do too much. Start read-only; add writes deliberately, validate arguments, and log calls.
4 · Policy in the repository. Anyone with write access can edit it. Non-negotiable rules belong in managed settings.
5 · Assuming the sandbox covers everything. Hooks, local MCP servers and the built-in file tools run outside it; it is also off by default.
6 · A plugin you have not read. It runs as you in every session. Read before installing; check its context cost.
7 · Packaging too early. A plugin for one repository is overhead. Use project files until a second repository needs the same setup.
11
Summary

Key Takeaways, Exercises and Sources

∑ Your own environment — what to remember
  • Think in layers: organisation policy, plugins, project files, personal files, session.
  • Skills teach how; MCP servers provide tools. A stdio MCP server is a short program that speaks JSON-RPC.
  • Test servers and hooks without a client: pipe in the protocol or the hook JSON and check the output.
  • Prefer CLIs for systems that have them; use MCP for those that do not.
  • Stack the boundaries: permission rules, sandbox, container — each covers different things.
  • Enterprise policy must live outside the repository (server-managed or MDM) to be policy.
Exercises

Exercise 1 Beginner

Run the MCP protocol test from Section 3. Then change the server so feature_status also reports the count of failing features, and re-run the test to confirm the new text.

Learning outcome: see a tool's whole lifecycle with no client involved.

Exercise 2 Intermediate

Add a second tool, list_failing_features, with an inputSchema that takes an optional limit. Make it return an error result (isError: true) for a negative limit rather than a protocol error, and explain from the spec why that is the right category.

Learning outcome: distinguish tool execution errors from protocol errors.

Exercise 3 Advanced

Write a managed-settings policy for a fictional team of 20 developers: what you deny, how logins are restricted, which providers are allowed, and which delivery route you would use. For each key, name the failure it prevents and how you would verify it is enforced.

Learning outcome: design policy that is enforceable, not merely documented.

Sources (read on 2026-10-30; paraphrased, at most one short quote per source)
  1. Anthropic, Extend Claude Code — how features layer; context cost.
  2. Anthropic, Deploy managed settings — precedence, delivery mechanisms, file locations. Read in part (the page is long).
  3. Anthropic, Skills — frontmatter fields, locations, invocation.
  4. Anthropic, Monorepos and large codebases — skill discoverability and description truncation.
  5. Model Context Protocol, specification revision 2025-11-25, Lifecycle, Transports and Tools pages (raw sources in the specification repository). The project's website was not reachable from this environment, so the GitHub sources were read instead; a newer revision may exist.
  6. Anthropic, MCP — scopes, .mcp.json, approval, security.
  7. Anthropic, Sandboxing — what is isolated, platforms, limitations, what it does not cover. The last ~7,500 characters of the page were not read.
  8. Anthropic, Hooks — events, handler types, exit codes.
  9. Anthropic, Best practices — hook patterns, CLIs versus MCP, deterministic enforcement.
  10. Anthropic, Manage costs — MCP overhead; prefer CLI tools.
  11. Anthropic, Plugins overview — components, manifest, scopes, cost, marketplaces.
  12. Anthropic, Permissions — rule evaluation, enforcement by Claude Code.
  13. Anthropic, Development containers — setup, warnings, policy in a container.
  14. Anthropic, Deploy managed settings (as above).
  15. Anthropic, Authentication — forceLoginMethod, forceLoginOrgUUID, allowedProviders behaviour with API keys.
  16. Anthropic, Manage costs — organisation spend controls and OpenTelemetry.
  17. Anthropic, Data usage — telemetry opt-outs.

Executed on this machine, 2026-10-30: the MCP protocol exchange with Node v24.15.0 and jq; PyYAML parse of skill frontmatter; jq validation of the plugin manifest, .mcp.json and the managed-settings file. Not executed: connecting the server to Claude Code, installing the plugin, deploying managed settings. The managed-settings keys shown were each named in the cited pages; the file as a whole was not checked against a live deployment.