Skip to content
AI Intermediate Tutorial

Automate Multi-File Refactors with Claude Code Subagents

Build a scout-and-surgeon subagent pipeline behind a /refactor command that cannot touch your test suite.

Mariana Souza
Mariana Souza
Senior Editor · Sep 6, 2026 · 6 min read
Automate Multi-File Refactors with Claude Code Subagents

What you'll build

A two-subagent refactor pipeline in Claude Code: a read-only scout that inventories every call site, a surgeon that edits one file at a time, and a /refactor slash command that drives both behind a permission boundary that can't touch your tests.

Prerequisites

Verified on Claude Code 2.1.263 (macOS/Linux/WSL) and Node.js 20.19.6. Node 22+ if you install Claude Code through npm.

claude --version   # 2.1.263 (Claude Code)
node --version     # v20.19.6 or later

You need a Claude Pro, Max, Team, Enterprise, or Console account. The Edit-rule startup warning in Troubleshooting requires 2.1.210 or later.

1. Seed a demo repo

Three modules log with string concatenation. A shared logger exists but nobody calls it. The test encodes the target state, so it fails until the refactor lands.

mkdir refactor-demo && cd refactor-demo && git init -q
mkdir -p src/lib test
echo '{"name":"refactor-demo","type":"module","private":true}' > package.json

cat > src/lib/logger.js <<'EOF'
export const records = [];

export const logger = {
  info(event, fields = {}) {
    records.push({ event, ...fields });
  },
};
EOF

cat > src/checkout.js <<'EOF'
export function checkout(cartId, total) {
  console.log("checkout " + cartId + " total=" + total);
  return { cartId, total };
}
EOF

cat > src/orders.js <<'EOF'
export function placeOrder(orderId, cartId) {
  console.log("order placed " + orderId + " from " + cartId);
  return { orderId, cartId };
}
EOF

cat > src/users.js <<'EOF'
export function createUser(userId, email) {
  console.log("user created " + userId + " <" + email + ">");
  return { userId, email };
}
EOF

cat > test/logging.test.js <<'EOF'
import test from "node:test";
import assert from "node:assert/strict";
import { records } from "../src/lib/logger.js";
import { checkout } from "../src/checkout.js";
import { placeOrder } from "../src/orders.js";
import { createUser } from "../src/users.js";

test("every module logs through the shared logger", () => {
  records.length = 0;
  checkout("cart_1", 42);
  placeOrder("ord_1", "cart_1");
  createUser("u_1", "a@example.com");
  assert.deepEqual(
    records.map((r) => r.event),
    ["checkout", "order_placed", "user_created"],
  );
});
EOF

git add -A && git commit -qm "seed"

2. Write the read-only scout

Subagents are Markdown files with YAML frontmatter in .claude/agents/. Omitting tools inherits everything, so name them explicitly; that's what keeps this one incapable of writing.

mkdir -p .claude/agents
cat > .claude/agents/refactor-scout.md <<'EOF'
---
name: refactor-scout
description: Inventories every call site for a refactor and returns a file-by-file plan. Read-only.
tools: Read, Grep, Glob
model: sonnet
color: cyan
---

You map refactors. You never edit files.

Given a scope glob and a goal:

1. Glob the scope, then Grep for every construct the goal touches.
2. Read each match with enough surrounding lines to see the call shape.
3. Return a Markdown table with columns: file, line, current call, replacement, risk.
4. End with a line starting `BLOCKERS:` listing anything ambiguous — dynamic call
   sites, re-exports, generated files — or `BLOCKERS: NONE`.

Report only. Do not propose a diff.
EOF

3. Write the surgeon

One file per invocation. Narrow, stateless tasks can run in parallel without stepping on each other, and a bad edit stays one file wide.

cat > .claude/agents/refactor-surgeon.md <<'EOF'
---
name: refactor-surgeon
description: Applies one file's rows from an approved refactor plan. Use after refactor-scout.
tools: Read, Edit, Grep, Glob
disallowedTools: Bash
model: sonnet
color: orange
---

You apply exactly one file's edits from a plan you are handed.

- Edit only the file named in your task. If the plan implies changes elsewhere,
  report that instead of editing.
- Keep it minimal: no reformatting, no renames beyond the plan, no new dependencies.
- Add whatever import the replacement needs, with the correct relative path.
- Finish by reporting the lines you changed and anything you skipped.
EOF

Subagents start with no conversation history, so the surgeon only knows what the orchestrator pastes into its task. Hand it the plan rows verbatim.

4. Fence off the blast radius

Permission rules apply to subagents too, and deny beats allow. Denying Edit(test/**) turns the test suite into an oracle the agents can't rewrite.

cat > .claude/settings.json <<'EOF'
{
  "permissions": {
    "allow": [
      "Bash(node --test *)",
      "Bash(git status *)",
      "Bash(git diff *)",
      "Edit(src/**)"
    ],
    "deny": [
      "Edit(test/**)",
      "Edit(package.json)",
      "Bash(git push *)"
    ]
  }
}
EOF

Use Edit(...) for file rules, never Write(...). Only Edit and Read rules are consulted by file permission checks.

5. Add the /refactor slash command

A directory under .claude/skills/ becomes a slash command named after the directory. The !`cmd` syntax runs a shell command before Claude sees the file and injects the output.

mkdir -p .claude/skills/refactor
cat > .claude/skills/refactor/SKILL.md <<'EOF'
---
name: refactor
description: Scout a multi-file refactor, then apply it one file at a time
argument-hint: "[scope-glob] [goal]"
disable-model-invocation: true
allowed-tools: Bash(git status *) Bash(git diff *) Bash(node --test *) Read Grep Glob
---

## Working tree before we start

!`git status --short`

## Task

Full request: $ARGUMENTS
Scope glob: $0

1. Run the `refactor-scout` subagent over the scope glob with that goal. Wait for it.
2. Print its table. If the `BLOCKERS:` line is anything but `NONE`, stop and ask me.
3. Launch one `refactor-surgeon` subagent per file in the plan, in parallel, each
   given only that file's rows. They touch disjoint files.
4. Run `node --test test/`. Never edit a test to make it pass.
5. Print `git diff --stat`.
EOF

disable-model-invocation: true keeps Claude from firing this on its own. You invoke it or nobody does.

flowchart LR
  C["/refactor"] --> M[main session]
  M -->|Agent| S["refactor-scout<br/>Read Grep Glob"]
  S -->|plan table| M
  M -->|Agent x3| U["refactor-surgeon<br/>Read Edit Grep Glob"]
  U --> T["node --test test/"]

6. Run it

claude

Then, at the prompt:

/refactor src/**/*.js replace every console.log call with logger.info(event, fields) from src/lib/logger.js, using snake_case event names

Verify it works

Type /skills and refactor appears in the list. After the run finishes:

node --test test/
✔ every module logs through the shared logger (0.84ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 39.87

Confirm the blast radius held:

grep -rn "console.log" src/    # no output, exit 1
git diff --stat
 src/checkout.js | 4 +++-
 src/orders.js   | 4 +++-
 src/users.js    | 4 +++-
 3 files changed, 9 insertions(+), 3 deletions(-)

Three source files touched, test/ and package.json untouched. A surgeon that tried either would have been blocked by the deny rule.

Troubleshooting

/agents prints a reminder instead of opening a wizard. As of v2.1.198 the interactive creation UI is gone; it tells you to ask Claude or edit .claude/agents/ directly. Writing the file yourself, as above, is the supported path.

Permission deny rule (.claude/settings.json): Write(src/**) is not matched by file permission checks — only Edit(path) rules are. You wrote a path rule for Write, NotebookEdit, MultiEdit, or Glob. Claude Code keeps the rule but never consults it. Replace with Edit(src/**); Edit rules cover every file-editing tool.

Error: Shell command failed for pattern "!`node --test test/`" A ! injected command that exits non-zero aborts the whole skill invocation, and a failing test suite exits 1. Keep test runs in the skill body as an instruction (as in step 5) rather than an injected command, or append || true. grep, git diff, and find are exempt: exit code 1 is treated as normal for those.

A surgeon reports "I can't find the file you mentioned." Subagents receive no conversation history: only their system prompt, the task text, and CLAUDE.md. Paste the file path and the plan rows into the task instead of referring back to earlier turns.

Next steps

Add permissionMode: plan to the scout so it's read-only twice over. Set isolation: worktree on the surgeon to run edits in a throwaway git worktree. For large sweeps, CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS (default 20) caps the fan-out, and a SubagentStop hook in .claude/settings.json can run your linter after every file.

Sources & further reading

  1. Create custom subagents — code.claude.com
  2. Slash commands — code.claude.com
  3. Configure permissions — code.claude.com
  4. Error reference — code.claude.com
  5. Advanced setup — code.claude.com
  6. Node.js test runner — nodejs.org
Mariana Souza
Written by
Mariana Souza · Senior Editor

Mariana covers the fast-moving world of machine learning and generative AI, with a particular focus on how these technologies are reshaping development workflows. When she isn't stress-testing the latest foundation models, she's usually at a local hackathon.

Discussion 4

Join the discussion

Sign in or create an account to comment and vote.

Nina Petrova @night_owl_nina · 2 hours ago

scout and surgeon pattern is solid, but how does it handle circular dependencies or when the test boundary itself needs refactoring. feels like the permission model might become its own maintenance debt.

Sam Cole @junior_dev_sam · 6 hours ago

okay but what stops the surgeon from accidentally breaking stuff that isn't tests. like, how confident should i be running this on production code

Noor Haddad @indiehacker_noor · 8 hours ago

the scout-then-surgeon pattern is solid, wonder if this scales to monorepos or if you'd hit token limits fast

Will Carter @weekend_warrior_will · 26 minutes ago

the token limit thing is real, but i suspect you'd hit coordination chaos way before hitting claude's context window—like keeping state consistent across multiple surgeon passes in a monorepo sounds nightmarish without some solid transaction log between runs. i'm gonna spin this up on my homelab anyway and see where it actually breaks.

Related Reading