{"slug": "automate-multi-file-refactors-with-claude-code-subagents", "title": "Automate Multi-File Refactors with Claude Code Subagents", "summary": "A developer guide published by Mariana Souza on Sourcefeed demonstrates how to build a two-subagent refactor pipeline in Claude Code 2.1.263, using a read-only scout and a surgeon subagent behind a /refactor command that cannot modify the test suite. The tutorial includes code for a demo repo with Node.js 20.19.6 and requires a Claude Pro, Max, Team, Enterprise, or Console account.", "body_md": "# Automate Multi-File Refactors with Claude Code Subagents\n\nBuild a scout-and-surgeon subagent pipeline behind a /refactor command that cannot touch your test suite.\n\n[Mariana Souza](https://sourcefeed.dev/u/mariana_souza)\n\n## What you'll build\n\nA two-subagent refactor pipeline in [Claude Code](https://code.claude.com/docs/en/overview): 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.\n\n## Prerequisites\n\nVerified on **Claude Code 2.1.263** (macOS/Linux/WSL) and **[Node.js](https://nodejs.org) 20.19.6**. Node 22+ if you install Claude Code through npm.\n\n```\nclaude --version   # 2.1.263 (Claude Code)\nnode --version     # v20.19.6 or later\n```\n\nYou need a Claude Pro, Max, Team, Enterprise, or Console account. The `Edit`-rule startup warning in Troubleshooting requires 2.1.210 or later.\n\n## 1. Seed a demo repo\n\nThree 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.\n\n```\nmkdir refactor-demo && cd refactor-demo && git init -q\nmkdir -p src/lib test\necho '{\"name\":\"refactor-demo\",\"type\":\"module\",\"private\":true}' > package.json\n\ncat > src/lib/logger.js <<'EOF'\nexport const records = [];\n\nexport const logger = {\n  info(event, fields = {}) {\n    records.push({ event, ...fields });\n  },\n};\nEOF\n\ncat > src/checkout.js <<'EOF'\nexport function checkout(cartId, total) {\n  console.log(\"checkout \" + cartId + \" total=\" + total);\n  return { cartId, total };\n}\nEOF\n\ncat > src/orders.js <<'EOF'\nexport function placeOrder(orderId, cartId) {\n  console.log(\"order placed \" + orderId + \" from \" + cartId);\n  return { orderId, cartId };\n}\nEOF\n\ncat > src/users.js <<'EOF'\nexport function createUser(userId, email) {\n  console.log(\"user created \" + userId + \" <\" + email + \">\");\n  return { userId, email };\n}\nEOF\n\ncat > test/logging.test.js <<'EOF'\nimport test from \"node:test\";\nimport assert from \"node:assert/strict\";\nimport { records } from \"../src/lib/logger.js\";\nimport { checkout } from \"../src/checkout.js\";\nimport { placeOrder } from \"../src/orders.js\";\nimport { createUser } from \"../src/users.js\";\n\ntest(\"every module logs through the shared logger\", () => {\n  records.length = 0;\n  checkout(\"cart_1\", 42);\n  placeOrder(\"ord_1\", \"cart_1\");\n  createUser(\"u_1\", \"a@example.com\");\n  assert.deepEqual(\n    records.map((r) => r.event),\n    [\"checkout\", \"order_placed\", \"user_created\"],\n  );\n});\nEOF\n\ngit add -A && git commit -qm \"seed\"\n```\n\n## 2. Write the read-only scout\n\n[Subagents](https://code.claude.com/docs/en/sub-agents) 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.\n\n```\nmkdir -p .claude/agents\ncat > .claude/agents/refactor-scout.md <<'EOF'\n---\nname: refactor-scout\ndescription: Inventories every call site for a refactor and returns a file-by-file plan. Read-only.\ntools: Read, Grep, Glob\nmodel: sonnet\ncolor: cyan\n---\n\nYou map refactors. You never edit files.\n\nGiven a scope glob and a goal:\n\n1. Glob the scope, then Grep for every construct the goal touches.\n2. Read each match with enough surrounding lines to see the call shape.\n3. Return a Markdown table with columns: file, line, current call, replacement, risk.\n4. End with a line starting `BLOCKERS:` listing anything ambiguous — dynamic call\n   sites, re-exports, generated files — or `BLOCKERS: NONE`.\n\nReport only. Do not propose a diff.\nEOF\n```\n\n## 3. Write the surgeon\n\nOne file per invocation. Narrow, stateless tasks can run in parallel without stepping on each other, and a bad edit stays one file wide.\n\n```\ncat > .claude/agents/refactor-surgeon.md <<'EOF'\n---\nname: refactor-surgeon\ndescription: Applies one file's rows from an approved refactor plan. Use after refactor-scout.\ntools: Read, Edit, Grep, Glob\ndisallowedTools: Bash\nmodel: sonnet\ncolor: orange\n---\n\nYou apply exactly one file's edits from a plan you are handed.\n\n- Edit only the file named in your task. If the plan implies changes elsewhere,\n  report that instead of editing.\n- Keep it minimal: no reformatting, no renames beyond the plan, no new dependencies.\n- Add whatever import the replacement needs, with the correct relative path.\n- Finish by reporting the lines you changed and anything you skipped.\nEOF\n```\n\nSubagents start with no conversation history, so the surgeon only knows what the orchestrator pastes into its task. Hand it the plan rows verbatim.\n\n## 4. Fence off the blast radius\n\n[Permission rules](https://code.claude.com/docs/en/permissions) apply to subagents too, and deny beats allow. Denying `Edit(test/**)` turns the test suite into an oracle the agents can't rewrite.\n\n```\ncat > .claude/settings.json <<'EOF'\n{\n  \"permissions\": {\n    \"allow\": [\n      \"Bash(node --test *)\",\n      \"Bash(git status *)\",\n      \"Bash(git diff *)\",\n      \"Edit(src/**)\"\n    ],\n    \"deny\": [\n      \"Edit(test/**)\",\n      \"Edit(package.json)\",\n      \"Bash(git push *)\"\n    ]\n  }\n}\nEOF\n```\n\nUse `Edit(...)` for file rules, never `Write(...)`. Only `Edit` and `Read` rules are consulted by file permission checks.\n\n## 5. Add the /refactor slash command\n\nA directory under `.claude/skills/` becomes a [slash command](https://code.claude.com/docs/en/slash-commands) named after the directory. The `` !` cmd` `` syntax runs a shell command *before* Claude sees the file and injects the output.\n\n```\nmkdir -p .claude/skills/refactor\ncat > .claude/skills/refactor/SKILL.md <<'EOF'\n---\nname: refactor\ndescription: Scout a multi-file refactor, then apply it one file at a time\nargument-hint: \"[scope-glob] [goal]\"\ndisable-model-invocation: true\nallowed-tools: Bash(git status *) Bash(git diff *) Bash(node --test *) Read Grep Glob\n---\n\n## Working tree before we start\n\n!`git status --short`\n\n## Task\n\nFull request: $ARGUMENTS\nScope glob: $0\n\n1. Run the `refactor-scout` subagent over the scope glob with that goal. Wait for it.\n2. Print its table. If the `BLOCKERS:` line is anything but `NONE`, stop and ask me.\n3. Launch one `refactor-surgeon` subagent per file in the plan, in parallel, each\n   given only that file's rows. They touch disjoint files.\n4. Run `node --test test/`. Never edit a test to make it pass.\n5. Print `git diff --stat`.\nEOF\n```\n\n`disable-model-invocation: true` keeps Claude from firing this on its own. You invoke it or nobody does.\n\n``` php\nflowchart LR\n  C[\"/refactor\"] --> M[main session]\n  M -->|Agent| S[\"refactor-scout<br/>Read Grep Glob\"]\n  S -->|plan table| M\n  M -->|Agent x3| U[\"refactor-surgeon<br/>Read Edit Grep Glob\"]\n  U --> T[\"node --test test/\"]\n```\n\n## 6. Run it\n\n```\nclaude\n```\n\nThen, at the prompt:\n\n```\n/refactor src/**/*.js replace every console.log call with logger.info(event, fields) from src/lib/logger.js, using snake_case event names\n```\n\n## Verify it works\n\nType `/skills` and `refactor` appears in the list. After the run finishes:\n\n```\nnode --test test/\n✔ every module logs through the shared logger (0.84ms)\nℹ tests 1\nℹ suites 0\nℹ pass 1\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 39.87\n```\n\nConfirm the blast radius held:\n\n```\ngrep -rn \"console.log\" src/    # no output, exit 1\ngit diff --stat\nsrc/checkout.js | 4 +++-\n src/orders.js   | 4 +++-\n src/users.js    | 4 +++-\n 3 files changed, 9 insertions(+), 3 deletions(-)\n```\n\nThree source files touched, `test/` and `package.json` untouched. A surgeon that tried either would have been blocked by the deny rule.\n\n## Troubleshooting\n\n**`/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.\n\n**`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.\n\n**``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.\n\n**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.\n\n## Next steps\n\nAdd `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.\n\n## Sources & further reading\n\n1. \n                                    [Create custom subagents](https://code.claude.com/docs/en/sub-agents)\n                                — code.claude.com\n2. \n                                    [Slash commands](https://code.claude.com/docs/en/slash-commands)\n                                — code.claude.com\n3. \n                                    [Configure permissions](https://code.claude.com/docs/en/permissions)\n                                — code.claude.com\n4. \n                                    [Error reference](https://code.claude.com/docs/en/errors)\n                                — code.claude.com\n5. \n                                    [Advanced setup](https://code.claude.com/docs/en/setup)\n                                — code.claude.com\n6. \n                                    [Node.js test runner](https://nodejs.org/api/test.html)\n                                — nodejs.org\n\n[Mariana Souza](https://sourcefeed.dev/u/mariana_souza)· Senior Editor\n\nMariana 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.\n\n## Discussion 5\n\nthe scout-then-surgeon pattern is clever. wonder how this would handle cross-file type dependencies in a statically-typed lang though\n\nscout 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.\n\nokay but what stops the surgeon from accidentally breaking stuff that isn't tests. like, how confident should i be running this on production code\n\nthe scout-then-surgeon pattern is solid, wonder if this scales to monorepos or if you'd hit token limits fast\n\nthe 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.", "url": "https://wpnews.pro/news/automate-multi-file-refactors-with-claude-code-subagents", "canonical_source": "https://sourcefeed.dev/a/automate-multi-file-refactors-with-claude-code-subagents", "published_at": "2026-09-06 11:47:03+00:00", "updated_at": "2026-09-07 01:59:58.430982+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents", "ai-tools"], "entities": ["Claude Code", "Mariana Souza", "Sourcefeed", "Node.js"], "alternates": {"html": "https://wpnews.pro/news/automate-multi-file-refactors-with-claude-code-subagents", "markdown": "https://wpnews.pro/news/automate-multi-file-refactors-with-claude-code-subagents.md", "text": "https://wpnews.pro/news/automate-multi-file-refactors-with-claude-code-subagents.txt", "jsonld": "https://wpnews.pro/news/automate-multi-file-refactors-with-claude-code-subagents.jsonld"}}