Maintained
Claude Code Hooks, MCP, and Automation: Build a Verifiable Agent Harness
Choose the right Claude Code extension mechanism, write fail-safe hooks, constrain MCP trust, and run headless or GitHub Actions workflows with deterministic validation.
- Claude Code
- Hooks
- MCP
- Automation
- GitHub Actions
A Claude Code harness should make repeated work more reliable, not give a model more ways to create unobserved side effects. The useful design separates judgment from enforcement: instructions and skills guide decisions, hooks react deterministically to lifecycle events, MCP exposes reviewed external capabilities, and CI validates the resulting change outside the model.
This guide reflects Anthropic’s official documentation checked on August 9, 2026. Hook events, input schemas, and automation flags have changed across client versions; pin a known client for critical automation and test the effective configuration before rollout.
Give each extension one job
Claude Code has overlapping-looking extension points. Choose by behavior:
| Mechanism | Use it for | Do not treat it as |
|---|---|---|
CLAUDE.md | Short, always-relevant project rules | A security boundary or procedure library |
| Skill | A reusable workflow loaded when relevant | An unconditional lifecycle trigger |
| Hook | A deterministic action at a named event | A reasoning agent or complete sandbox |
| MCP server | A reviewed connection to external tools or data | Trusted content merely because it is structured |
| Subagent | Context-isolated investigation or delegated task | File isolation unless a worktree is configured |
claude -p / Agent SDK | Programmatic execution and structured results | Proof that the requested outcome is correct |
| GitHub Action | Remote trigger, environment, and permission envelope | Permission to merge every generated patch |
Start with a manual workflow and its validators. Automating a task before its inputs, stop conditions, and acceptance checks are stable makes failures faster rather than rarer.
Build the harness around an evidence pipeline
A robust automation has distinct stages:
trusted trigger
→ normalized task brief
→ bounded Claude execution
→ structured result
→ deterministic validation
→ human or policy gate
→ authorized side effect
Do not let the same natural-language response serve as task definition, execution log, validation, and approval. Store exact requirements in a reviewable artifact. Capture the input commit, environment, allowed tools, timeout, output, changed paths, tests, and final commit or patch.
An automation should be able to answer:
- What caused this run?
- Which repository state did it use?
- What could the process read, write, and contact?
- What changed?
- Which validators actually ran, and with what exit codes?
- Which step, if any, created an external side effect?
Use hooks for deterministic lifecycle behavior
Hooks receive JSON on standard input and may return decisions or context. They are appropriate for narrow checks such as blocking a known destructive command, recording tool duration, validating a changed path, or running a small formatter after a successful edit.
PreToolUse runs after Claude has prepared a tool call and before the call executes. It can allow, deny, ask, modify input, or add context. PostToolUse runs after a successful call; it cannot undo the action. Select the event by when enforcement must happen.
A critical current detail is exit behavior. For most hook events, exit code 2 is the blocking signal. Exit code 1 is normally a non-blocking hook error and the action proceeds. If a hook enforces policy, test the exact event and output contract instead of relying on conventional Unix failure semantics.
Prefer structured JSON decisions when the hook needs richer behavior. A minimal blocking shell hook might be easy to read, but shell prefix matching is not a complete command parser:
#!/usr/bin/env bash
set -u
input=$(jq -e '.' < /dev/stdin) || {
echo "invalid hook input" >&2
exit 2
}
command=$(jq -r '.tool_input.command // empty' <<<"$input")
case "$command" in
"git push --force"*|"git reset --hard"*)
echo "history-changing Git command requires a separate workflow" >&2
exit 2
;;
esac
exit 0
This example blocks two exact families; it does not prove that every equivalent destructive shell expression is blocked. Use Claude Code permissions, sandboxing, OS controls, and protected branches for the broader boundary.
Make hook input handling hostile by default
Tool parameters, prompts, filenames, MCP results, and repository contents may contain untrusted text. Never interpolate a JSON field into an eval, unquoted shell command, SQL statement, URL, or filesystem path.
For every command hook:
- parse stdin with a real JSON parser;
- validate expected event and tool names;
- normalize and bound paths before access;
- use argument arrays where possible;
- cap runtime and output size;
- keep stdout reserved for the documented response format;
- send diagnostics to stderr without secrets;
- fail in the intended direction for that event;
- test malformed, missing, oversized, and adversarial inputs.
On Windows, file paths delivered to hooks use backslashes. Normalize separators before comparing path segments. Also remember that PreToolUse for Read does not intercept a file included directly through an @ reference; use a read deny rule when a path must not enter context.
Keep hooks short. If logic becomes a policy engine, move it to a versioned script with unit tests and make the hook a thin adapter.
Connect MCP at the narrowest useful scope
MCP can expose remote services, local processes, resources, prompts, and tools. Review the implementation and its authority, not only its name or description.
Claude Code distinguishes MCP installation scopes:
- local scope is private to one user in one project and is the default;
- project scope uses
.mcp.jsonand is designed for a team to share; - user scope applies across that user’s projects;
- managed policy and plugins can add or constrain servers for an organization.
Use local scope for experiments and credentials that must not enter version control. Use project scope only when teammates can review the server command or endpoint and the configuration contains variable references rather than secrets.
Before enabling a server, record:
| Area | Review question |
|---|---|
| Publisher | Who maintains the server and update channel? |
| Transport | Which local process or remote endpoint runs? |
| Authentication | Which identity and scopes are used? |
| Tools | Which reads and writes can each tool perform? |
| Data | What can the server return into model context? |
| Network | Which destinations can the server and its dependencies reach? |
| Failure | How are partial writes, retries, and idempotency handled? |
| Audit | Can an operator reconstruct calls and outcomes without logging secrets? |
MCP output is untrusted data. An issue, database row, web page, or support ticket may contain prompt injection. Enforce authorization before retrieval, keep write tools separately restricted, and validate the final side effect outside the model.
Prefer explicit MCP commands and variables
Use the current CLI rather than hand-editing undocumented state:
claude mcp add --transport http example https://mcp.example.com/mcp
claude mcp list
claude mcp get example
The first command creates a local-scoped server by default. For a shared project definition, use the documented project scope and environment-variable expansion. Do not commit a bearer token in .mcp.json.
Test a server with a read-only identity first. Confirm tool names and schemas, then exercise a denied operation and a partial failure. A successful connection only proves transport and authentication; it does not prove that authorization or data classification is correct.
Run headless Claude with bounded authority
Programmatic mode uses claude -p. For automation, request machine-readable output and allow only the tools the task needs:
claude --bare -p "Inspect the changed files and return a review report. Do not edit." \
--allowedTools "Read,Glob,Grep" \
--output-format json
--bare starts with a reduced feature set for lower overhead. --output-format json includes the result, session identifier, and metadata; stream-json emits incremental events. When downstream code needs a fixed shape, combine JSON output with --json-schema and validate the returned object again in your own process.
Treat exit code zero and fluent text as necessary but insufficient. Inspect permission denials and structured errors, confirm the process ended, capture untracked new files as well as normal Git diffs, and run deterministic checks independently.
Bound every run:
- fixed repository and working directory;
- clean, named input commit;
- explicit tools and network policy;
- wall-clock timeout and retry cap;
- model and usage budget appropriate to the task;
- maximum output and artifact size;
- no deployment, merge, send, payment, deletion, or access change without a separate gate.
Retries should create new evidence. Repeating the same request after a deterministic test failure is not a recovery strategy.
Put deterministic checks outside the agent loop
Claude can write or run tests, but the orchestration layer should decide whether required checks passed. A safe sequence is:
set -euo pipefail
npm ci
npm run lint
npm run check
npm test -- --run
npm run build
git diff --check
Use the repository’s real commands. Preserve exit codes and the relevant output. Run focused tests while iterating, then the required suite on the final combined state.
Test quality also matters. For a bug fix, show that the regression test fails on the original defect and passes on the fix. Reject patches that delete, skip, loosen, or replace assertions merely to obtain green output.
Design GitHub Actions as a least-privilege gate
Anthropic provides a Claude Code GitHub Action and a GitHub App flow. The official app has a permission set shared by multiple Claude features; GitHub does not let an installer accept only a subset. Organizations that require a narrower installation can follow Anthropic’s custom-app guidance for the action’s required repository permissions.
At the workflow level, grant only what the job needs. A reporting job can use read permissions; a patch-producing job may need branch writes but should not receive deployment or organization administration authority.
Protect credentials with GitHub Secrets or an approved cloud identity. Do not expose secrets to untrusted pull-request code, echo them in logs, or make them available to setup commands from an unreviewed branch. Pin actions according to the organization’s supply-chain policy and review updates.
Use trigger filters and concurrency controls to prevent duplicate expensive runs. Put generated changes on a branch and require the same tests and reviews as a human contribution. An agent opening a pull request is a handoff, not authorization to merge it.
Observe behavior without collecting secrets
Record operational evidence that helps diagnose failures:
- task ID and trusted trigger;
- source and result commit SHAs;
- Claude Code version, model, and execution mode;
- enabled skills, hooks, MCP servers, and allowed tools;
- command duration and exit status;
- token or plan usage where available;
- changed paths, validator results, and final disposition.
Redact before storage. Avoid logging raw prompts, full environments, MCP payloads, request bodies, or transcripts when they may contain credentials, personal data, proprietary source, or injected content.
Create alerts for repeated permission denial, unexpected network destinations, new changed-path categories, validator bypass attempts, abnormal cost, and runs that end without a complete result.
Roll out automation in four stages
- Observe: Claude produces a read-only report; a human performs every side effect.
- Propose: Claude creates a local patch; deterministic checks and a human decide whether to commit.
- Deliver: automation pushes a dedicated branch and opens a PR; protected review and CI govern merge.
- Constrain autonomy: only a mature, reversible task may cross a side-effect gate automatically, with monitoring and an immediate stop path.
Advance based on accepted outcomes and incident evidence, not the number of successful demos. Keep a manual fallback and rehearse credential rotation, job cancellation, branch cleanup, and partial-write reconciliation.
Final checklist
- Each extension mechanism has one clear job.
- Hooks parse structured input and use the correct blocking contract.
- Hook scripts have adversarial and platform-specific tests.
- MCP servers use the narrowest scope, credentials, and tools.
- Retrieved MCP data is treated as untrusted.
- Headless runs have bounded time, tools, network, output, and retries.
- Structured output is parsed and independently validated.
- CI permissions and secrets match the exact job.
- Generated code goes through deterministic checks and review.
- External side effects have a separate authorization gate and audit trail.
Continue with the permissions and security guide, Git and parallel workflows, and context, models, and cost guide.