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:

  1. Permission prompts. In default mode, 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.
  2. 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 a PreToolUse hook would otherwise allow one. Reach for this before the first prompt on an unfamiliar repo.
  3. 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

Share.

Haythem Rehouma builds and teaches DevOps and AI tooling: Kubernetes, Docker, Terraform, Claude Code, ChatGPT and n8n. Every KubPoint guide is written from the official docs and re-tested before it goes live.

Comments are closed.