An agentic workflow is a plan-act-observe loop wired to a real tool, a real check, and a real stop condition, not a diagram. This lab wires one with Claude Code: an MCP server that adds a tool, a PreToolUse hook that refuses the risky calls that tool allows, a PostToolUse/Stop pair that runs lint and tests after every edit, and three places a human still has to act. Every command below comes from the official Claude Code docs, the MCP reference servers, or GitHub’s own MCP server repository.
Step 1 — Give the agent a tool with MCP
Claude Code doesn’t call tools directly; it reads a tool list a client assembled, and MCP is how that list grows. The official MCP registry maintains two reference servers that cover most of a DevOps lab’s needs: a filesystem server and a GitHub server, both shipped by the steering group or by GitHub itself, not a random third-party package.
Filesystem server. Published as @modelcontextprotocol/server-filesystem, it reads, writes, moves, and searches files under directories named on its command line. That argument is documented as the one method that works with every protocol version the server supports; a client-supplied directory list (Roots) is a deprecated convenience for older clients, not something to depend on. Add it as a project-scoped server so the entry lives in a .mcp.json the team can review:
claude mcp add --scope project --transport stdio filesystem -- npx -y @modelcontextprotocol/server-filesystem "$CLAUDE_PROJECT_DIR"
That writes:
{
"mcpServers": {
"filesystem": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "${CLAUDE_PROJECT_DIR}"]
}
}
}
GitHub server. GitHub maintains github/github-mcp-server, distributed as a Docker image at ghcr.io/github/github-mcp-server, and recommends keeping the personal access token out of the config file by passing it through the environment:
export GITHUB_PAT=your_token_here
claude mcp add github -e GITHUB_PERSONAL_ACCESS_TOKEN=$GITHUB_PAT -- docker run -i --rm -e GITHUB_PERSONAL_ACCESS_TOKEN ghcr.io/github/github-mcp-server
A hosted alternative exists at https://api.githubcopilot.com/mcp/ if you would rather not run the container locally; either way, scope the token narrowly, since the server can reach most of the GitHub API that token allows.
Open a session and confirm the server loaded with /mcp. Hosts, clients, and transports are covered in MCP servers explained; this lab goes straight to the controls around the tool.
Step 2 — Block the dangerous calls with a PreToolUse hook
An MCP tool shows up in hook events under a fixed name pattern: mcp__<server>__<tool>. The filesystem server’s write tools are write_file and edit_file, so the full names are mcp__filesystem__write_file and mcp__filesystem__edit_file. A matcher built only from letters, digits, underscores, and | is compared as an exact list of strings, so this matcher hits only those two tools and nothing with a similar prefix:
.claude/hooks/guard-fs-write.sh:
#!/bin/bash
# Deny filesystem MCP writes outside the project's scratch/ directory.
input=$(cat)
path=$(printf '%s' "$input" | jq -r '.tool_input.path // empty')
scratch="${CLAUDE_PROJECT_DIR:-.}/scratch"
case "$path" in
"$scratch"/*) exit 0 ;;
"") exit 0 ;;
*)
echo "Blocked: $path is outside scratch/. Widen the hook on purpose if this edit is intended." >&2
exit 2
;;
esac
{
"hooks": {
"PreToolUse": [
{
"matcher": "mcp__filesystem__write_file|mcp__filesystem__edit_file",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/guard-fs-write.sh"
}
]
}
]
}
}
Exit 2 cancels the call and hands the agent the stderr line as the reason, so a loop that keeps reaching outside scratch/ gets redirected instead of silently refused. The same syntax covers the GitHub server: mcp__github__.* is a regular expression (it contains a dot) matching every tool that server exposes; narrow it further to gate only the tools that write.
This is the same mechanism the built-in Edit and Write tools use, covered in the hooks guide; an MCP tool is just a longer name on the same tool_name field.
Step 3 — Run the validation pipeline automatically
A PostToolUse hook runs after a tool call already succeeded, which is the right point for a formatter or a narrow test run, not for preventing the write. Chain it after both the built-in edit tools and the filesystem MCP tool so neither path skips the check:
.claude/hooks/validate-change.sh:
#!/bin/bash
# Lint, then test, the file Claude just touched. Block the result on failure.
input=$(cat)
path=$(printf '%s' "$input" | jq -r '.tool_input.path // .tool_input.file_path // empty')
[ -f "$path" ] || exit 0
case "$path" in
*.py)
ruff check "$path" >&2 || exit 2
pytest "$path" >&2 || exit 2
;;
*.ts|*.js)
npx eslint "$path" >&2 || exit 2
npm test -- "$path" >&2 || exit 2
;;
esac
exit 0
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|mcp__filesystem__write_file|mcp__filesystem__edit_file",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/validate-change.sh",
"timeout": 120
}
]
}
]
}
}
A Stop hook closes the gap a per-file check leaves: nothing above re-runs the whole suite. Stop fires on every turn, ignores matchers entirely, and can block the end of the turn until it exits 0:
.claude/hooks/require-green-suite.sh:
#!/bin/bash
# Block the end of the turn until the full test suite is green.
if ! npm test >&2; then
echo "Full suite is red. Fix the failure before the turn ends." >&2
exit 2
fi
exit 0
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/require-green-suite.sh",
"timeout": 180
}
]
}
]
}
}
Claude Code stops honoring a blocking Stop hook after eight consecutive blocks with no tool call in between, so this is a backstop against a flaky test, not infinite retry. A sub-agent is the other documented shape for this job: a code-reviewer agent with read-only tools, called after a batch of edits, reports blocking and non-blocking findings without touching files itself. The subagents guide has the file format; use a Stop hook for a deterministic pass/fail check, a sub-agent for a judgment call a script can’t make.
Step 4 — Put a human in three specific places
Hooks keep the loop inside a lane. They don’t replace a person deciding the lane is correct. Three checkpoints are built into Claude Code and don’t need a custom script:
- Permission prompts. In
defaultmode, Claude Code asks before the first use of each tool in a session, including an MCP tool the project just added. That single prompt is still the moment to notice a server that shouldn’t have been approved. - Plan mode.
claude --permission-mode plan,/plan, or Shift+Tab puts the session into a read-only research pass: Claude reads files and explores but does not edit, and edits stay blocked until the plan is approved, even if aPreToolUsehook would otherwise allow one. Reach for this before the first prompt on an unfamiliar repo. - Pull request review. Point the GitHub MCP server at opening a branch and a draft pull request instead of pushing to a default branch, and require the usual human approval before merge. The validation pipeline is the gate before the PR exists; review is the gate before it lands.
None of this is a sandbox by itself. Keep bypassPermissions for an isolated container or VM, as the permission-mode documentation itself scopes it, not for a session running against your working tree.
The loop end to end
┌────────────────────────────────────────────────┐
│ │
▼ │
┌───────────┐ ┌───────────┐ ┌────────────┐ ┌─────────────┐
│ PLAN │ --> │ ACT │ --> │ VALIDATE │ --> │ HUMAN GATE │
│ plan mode │ │ MCP tool │ │ PostToolUse│ │ permission │
│ read-only │ │ call, │ │ lint+test, │ │ prompt, PR │
│ research │ │ guarded │ │ Stop hook │ │ review │
│ │ │ by │ │ re-runs │ │ │
│ │ │ PreToolUse│ │ full suite │ │ │
└───────────┘ └───────────┘ └─────┬──────┘ └──────┬──────┘
│ exit 2 │ reject plan
└── keep acting ◄────────┘
Plan mode is the entry; PreToolUse sits on every MCP call inside act; PostToolUse and Stop are validate; the permission prompt, the plan approval, and the PR review are the three human gates. A failing Stop hook sends the loop back to act, not around the human gate.
An n8n AI Agent node builds the same loop outside a coding agent, with workflow nodes standing in for hooks; see the first n8n workflow for an HTTP-shaped target instead of a repo. A team formalizing validation as written specs rather than ad hoc tests can carry the same gate into spec-driven development.
Pitfalls
Runaway loops. The Stop hook’s eight-block limit is a backstop, not a design. The real control is scoping what act can reach first: a narrow MCP tool set, a scratch/ directory, and bypassPermissions reserved for a container you can throw away.
Tool permissions too broad. The filesystem server’s allowed-directory list only covers paths passed on its command line; don’t rely on a client’s Roots support instead, since that is the one method guaranteed to work on every protocol version. For GitHub, scope the token to the repos and actions the lab needs, not a token with full account access.
Secrets in MCP env. Pass GITHUB_PERSONAL_ACCESS_TOKEN through the server’s own env, as above, never inline in a committed .mcp.json. Project scope is shared through git on purpose; a token baked into that file ships to everyone who clones the repo.
Cost. Every MCP tool call, hook invocation, and sub-agent review is its own unit of work against the session. A Stop hook that reruns a slow suite every turn, or a loop that keeps retrying a blocked edit, adds up fast; keep the full-suite check in Stop, not PostToolUse, and keep the per-file check narrow.
Treat the whole pipeline as something to write down once it works, the same way an AI governance checklist expects a team to record which tools an agent can reach and who approves what it changes.
FAQ
What is an agentic workflow?
A loop where a model plans a step, calls a tool, reads the result, and decides the next step, instead of returning one answer. It keeps going until no tool call is left, a check fails, or a human stops it. The pieces that make it safe to run are the tool boundary, a validation step, and at least one point where a person has to say yes.
How do I give Claude Code a new tool without writing a server?
Add an official reference MCP server. The filesystem server (@modelcontextprotocol/server-filesystem) and the GitHub server (github/github-mcp-server) are maintained by the protocol’s steering group and by GitHub. Register either with claude mcp add, or write the entry into .mcp.json yourself.
Which hook event blocks a dangerous action before it runs?
PreToolUse. Give it a matcher for the exact tool, including an MCP tool’s full mcp__server__tool name, and exit 2 from the hook script with a reason on stderr. A JSON permissionDecision of deny does the same without relying on the exit code.
Does a Stop hook replace a human reviewing the diff?
No. A Stop hook can block the end of a turn until a command exits zero, which is useful for a test suite, but Claude Code gives up after eight consecutive blocks with no tool call between them. Permission prompts, plan mode, and a pull request review are the actual human checkpoints; the hook only keeps the loop going long enough to reach one.
How do I keep the agent from looping forever?
Scope the MCP server’s own permissions first, for example the filesystem server’s allowed-directory argument, so a bad loop can only touch a small tree. Keep bypassPermissions for an isolated container, not your laptop. The Stop hook’s eight-block cap is a backstop, not the main control.
How this guide was verified
Written by hand from the official documentation, every command re-checked, and reviewed a second time before publishing. Last reviewed on .
Official sources read for this article
- code.claude.com/docs/en/mcp
- code.claude.com/docs/en/hooks
- code.claude.com/docs/en/hooks-guide
- code.claude.com/docs/en/permission-modes
- code.claude.com/docs/en/permissions
- code.claude.com/docs/en/agent-sdk/permissions
- github.com/modelcontextprotocol/servers
- github.com/modelcontextprotocol/servers/blob/main/src/filesystem/README.md
- github.com/github/github-mcp-server
- docs.github.com/en/copilot/how-tos/provide-context/use-mcp-in-your-ide/set-up-the-github-mcp-server

