DEV Community

Ahsan Ali
Ahsan Ali

Posted on Originally published at ahsanalidotme.substack.com

Running a Multi-Repo Product with Parallel AI Agents (Claude Code)

Running a Multi-Repo Product with Parallel AI Agents (Claude Code)

How I set up one workspace where a single Claude Code session hands work to one agent per repository, keeps them out of each other's code, and runs several tasks at the same time without them stepping on each other.

The setup: 7 independent git repos (API backend, customer dashboard, admin portal, marketing site, iOS SDK, Android SDK, an internal Python tool) sitting side by side in one folder.

1. The idea in one picture

workspace/                      ← its own small git repo (agents, rules, contracts)
├── CLAUDE.md                   ← map of all repos + shared rules
├── contracts/                  ← the ONLY place cross-repo changes are agreed
├── .claude/
│   ├── agents/<repo>.md        ← one agent per repo + a read-only test auditor
│   ├── hooks/                  ← guards that enforce the rules
│   └── settings.json
├── clone-all.sh                ← one command to get every repo
├── backend/                    ← separate repo (ignored by the workspace repo)
├── client-dashboard/           ← separate repo
└── ...
Enter fullscreen mode Exit fullscreen mode

You talk to one main session started at the workspace root. It plans, starts one agent copy per task per repo (each in its own git worktree), and collects the results.

2. Step by step

Step 1: Make the workspace root its own repo

Don't use submodules. Keep each product repo as a normal clone, and make the root a tiny repo that tracks only workspace files. Use an allowlist .gitignore so a product repo can never be committed by accident:

/*
!/.gitignore
!/README.md
!/CLAUDE.md
!/clone-all.sh
!/contracts/
!/.claude/
/.claude/settings.local.json
Enter fullscreen mode Exit fullscreen mode

Add a clone-all.sh that clones every repo into its folder, so a new machine or teammate gets the same layout with one command.

Step 2: A CLAUDE.md in every repo

Keep each one short (under 300 lines): purpose, stack, key folders, entry points, run/test/build commands, conventions. Generate it by having an agent read package.json, build files and the existing docs, then check the commands against the real scripts.

End each one with a pointer to the shared rules instead of copying them:

## Agent rules
Shared rules live in the workspace root ../CLAUDE.md.
In short: edit only this repo, put cross-repo needs in ../contracts/.
Enter fullscreen mode Exit fullscreen mode

Claude Code automatically loads CLAUDE.md files from parent folders, so the root rules apply inside every repo.

Step 3: The root CLAUDE.md

  • One line per repo: name, stack, role.
  • How the repos connect (who calls which API, which auth).
  • The core rule: route work to the matching agent; cross-repo changes go through /contracts.
  • A git workflow (branch from develop, never skip hooks, one PR per repo).
  • Parallel-work rules (see section 4).

Step 4: One agent type per repo, one copy per task

A file in .claude/agents/ defines an agent type, not a single worker. The main session can start many copies of the backend agent at once, one per task. Each copy works in its own git worktree, never in your main checkout.

.claude/agents/backend.md:

---
name: backend
description: "Use for any work in /backend: Node/Express API, database, webhooks, endpoints used by the dashboards and SDKs."
tools: Read, Edit, Write, Grep, Glob, Bash
---
You own the backend repo. Many copies of you can run at once, one per task,
so you never work in the main checkout.

1. Read backend/CLAUDE.md first, and follow it.
2. Work in your own worktree, one per task:
   git -C backend worktree add ../_wt/backend-<task> -b <type>/<task> origin/develop
   Install dependencies there for real; never symlink node_modules.
3. Edit only inside _wt/backend-<task>/. Never edit backend/ itself (it's the user's
   checkout) and never edit another repo.
4. Cross-repo needs: write contracts/<task>.md (owner, consumers, status), then stop.
5. Write tests for what you changed, including edge cases, and run them. Then reply
   with your worktree path so the main session can run test-auditor. Fix any gaps first.
6. Commit, push (hooks on, never --no-verify), open one PR to develop.
7. Append a 2-line summary to backend/.agent-notes.md. Final reply: worktree, branch,
   PR link, tests run, open contract items.
Enter fullscreen mode Exit fullscreen mode

The description is what routing uses, so make it specific ("Use for any work in /backend: …").

Step 5: A read-only test auditor that judges quality, not just presence

---
name: test-auditor
description: Use after a code change, before pushing, to audit one task's worktree. Read-only.
tools: Read, Grep, Glob, Bash
---
1. List what changed in the worktree vs origin/develop. Read the task.
2. Coverage: every new or changed behaviour has a test; no untested branch,
   error path or validation rule.
3. Missing edge cases: name the specific ones that should exist but don't:
   empty/null, boundaries (0, 1, max), invalid input, auth/permission failures,
   cross-tenant access, duplicates/idempotency, retries/timeouts, dates/time zones,
   pagination, UI loading/empty/error states.
4. Value: flag tests that would still pass if the code were broken: no real
   assertion, asserting a mock's own return value, testing implementation details,
   snapshot-only logic, copy-paste cases, over-mocking the thing under test.
5. Run the tests. Output PASS, or GAPS: one specific bullet per gap, most important first.
Enter fullscreen mode Exit fullscreen mode

It has no Edit or Write tools, so it reports problems instead of quietly "fixing" them. Each gap is specific enough for the repo agent to write the missing test.

Step 6: /contracts for cross-repo work

A folder of small spec files, one per topic. Each says: owner repo, consumers, status (proposed / agreed / implemented), and the shape (types, endpoints, examples). An agent never edits another repo; it writes the need here, and the owning agent implements it.

Step 7: Enforce the rules with hooks (instructions alone aren't enough)

Rules written in CLAUDE.md are suggestions. Hooks are code the harness runs every time.

PreToolUse guard: a repo agent may edit only its own worktrees, contracts/, and its notes file. Not other repos, not other agents' worktrees, and not your main checkout. Hook input includes agent_type when a subagent makes the call:

// .claude/hooks/repo-guard.js (trimmed)
const path = require('path');
let raw = '';
process.stdin.on('data', (c) => (raw += c));
process.stdin.on('end', () => {
  let data;
  try { data = JSON.parse(raw); } catch { process.exit(0); } // fail open
  const file = (data.tool_input || {}).file_path;
  const agent = data.agent_type;                 // set only inside a subagent
  if (!file || !agent || agent === 'test-auditor') process.exit(0);

  const root = process.env.CLAUDE_PROJECT_DIR;
  const rel = path.relative(root, path.resolve(data.cwd || root, file));
  const [top, sub = ''] = rel.split(path.sep);
  const ownWorktree = top === '_wt' && sub.startsWith(`${agent}-`);  // _wt/backend-<task>/
  const notesFile = rel === path.join(agent, '.agent-notes.md');

  // Allowed: own worktrees, contracts/, own notes file. Not the main checkout.
  if (rel.startsWith('..') || !(ownWorktree || top === 'contracts' || notesFile)) {
    process.stdout.write(JSON.stringify({
      hookSpecificOutput: {
        hookEventName: 'PreToolUse',
        permissionDecision: 'deny',
        permissionDecisionReason: `Agent "${agent}": work in _wt/${agent}-<task>/; cross-repo needs go to contracts/.`,
      },
    }));
  }
  process.exit(0);
});
Enter fullscreen mode Exit fullscreen mode

PostToolUse formatter: each repo gets a hook that formats the file Claude just edited, using that repo's own pinned prettier (node_modules/.bin/prettier, not npx), limited to the file types its CI checks.

.claude/settings.json at the root:

{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Edit|Write|MultiEdit|NotebookEdit",
        "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/repo-guard.js\"" }] }
    ],
    "PostToolUse": [
      { "matcher": "Edit|Write|MultiEdit",
        "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/format-dispatch.js\"" }] }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Also add a guard that blocks reading .env files. It caught me (the AI setting this up) trying to link a .env into a temp checkout, which is exactly what it's for.

Step 8: Keep local git hooks fast

Pre-push runs only lint and the formatter check (seconds). Typecheck, unit tests, build and E2E run in CI on the pull request, where branch protection gates the merge. Slow local hooks teach people (and agents) to use --no-verify, which then skips the fast checks too.

3. How a task flows

Start Claude Code at the workspace root and describe the task. Example: "Add utm_campaign to link analytics and show it on the dashboard."

  1. Plan. The main session sees that two repos are involved and that it's a cross-repo change.
  2. Contract first. A backend copy writes contracts/utm.md (owner, consumers, shape, status).
  3. Build each side in parallel. One backend copy in _wt/backend-utm/, one dashboard copy in _wt/client-dashboard-utm/, each on its own branch.
  4. Audit. Each copy runs its tests and returns its worktree path. The main session runs test-auditor on it, and gaps go back to that same copy.
  5. Ship. Each copy pushes (lint + format hooks) and opens its PR. CI runs the heavy checks.
  6. Report. The main session summarizes with PR links and removes the worktrees.

Subagents can't start other subagents, so every handoff goes through the main session. You only talk to that one.

4. Running many tasks at the same time

Every task gets its own agent copy and its own worktree, so tasks never share files or branches, even when ten of them touch the same repo.

Example: 10 tasks, 8 touching backend + dashboard and 2 backend-only:

8 full-stack tasks → 8 backend copies + 8 dashboard copies (same task name, joined by a contract)
2 backend-only     → 2 backend copies
                     = 18 agent copies, each in its own _wt/<repo>-<task>/ worktree
Enter fullscreen mode Exit fullscreen mode

Limits and what to do about them:

  • Machine load (installs, builds): run in waves of ~4 copies; keep local hooks to lint + format.
  • Tests that share one local database: run that repo's test suite one copy at a time.
  • Token cost and rate limits: waves; the main session only keeps each copy's short summary.
  • Task B needs task A's API: contract first; list dependencies in the prompt.

A prompt that works:

Here are 10 tasks. Run one agent copy per task per repo, each in its own worktree. Cross-repo tasks go contract-first, then both sides in parallel. Max 4 copies at a time. Run test-auditor on every worktree before pushing, open one PR per repo per task, then give me one summary with the PR links.

5. Lessons learned (the painful ones)

  • Hooks can be silently off. Git runs nothing if core.hooksPath points to a folder that doesn't exist (for example, husky never installed in a stale checkout). CI failed on a formatting issue the pre-push hook would have caught. Check that .husky/_ exists, or run the hook once by hand.
  • A PostToolUse hook only sees Claude's file-edit tools. Files changed through shell scripts, or written before the hook existed, are not formatted. CI is still the backstop.
  • Check that a PR is still open before pushing to its branch. I pushed follow-up commits to PRs that had already been merged; they never reached develop and had to be moved to new PRs. This is now a written rule.
  • Never --no-verify to get past a hook, not even for a doc-only commit. If a hook can't run in a temp checkout, fix the checkout.
  • Don't symlink node_modules into a worktree. Next.js/Turbopack refuses a symlink that points outside the project. Do a real install (pnpm install --prefer-offline is fast thanks to the shared store).
  • Settings load only from the folder you start in. Root hooks apply when you start at the root; a repo's own .claude/settings.json applies when you start inside it. Ancestor CLAUDE.md files load either way.
  • Relative paths only. Absolute machine paths in shared docs break for every teammate.
  • Keep settings.local.json out of git. It holds personal permissions.

6. Checklist to copy

  1. Workspace root repo with an allowlist .gitignore + clone-all.sh
  2. Short CLAUDE.md per repo, with commands checked against real scripts
  3. Root CLAUDE.md: repo map, connections, routing rule, git workflow, parallel rules
  4. .claude/agents/<repo>.md (one type per repo, one copy + worktree per task) + a read-only test-auditor that checks edge cases and test value
  5. contracts/ with owner / consumers / status per spec
  6. PreToolUse repo guard + .env guard; PostToolUse formatter using each repo's pinned tools
  7. Pre-push = lint + format only; everything heavy in CI on the PR
  8. Guard blocks agents from the main checkout; waves of ~4 copies

Top comments (0)