Lately, across a few projects, the same repo has been modified by two AI coding tools at once, plus a few people pushing code themselves… At first I figured it was no big deal — it’s all just writing code, right? But after stumbling through it, I found the problems were almost never in the code itself; they were in collaboration mechanics — “who read which set of rules,” “who can touch what on the machine.” Here I’m writing down the boundaries I eventually drew.

First, a twist: the so-called “multiple parties”

Going through another one of my repos, I ran into something amusing. git shortlog -sn turned up two lines with two names, looking like two people pushing; a check of the email addresses showed they were the same person — the extra one was just my account name from clicking merge in the web UI. And of the 78 commits, 63 carry Co-Authored-By: Claude — the real “multiple parties” was never multiple people at all, it was one person plus several AI sessions. The traces of parallelism weren’t in .claude/worktrees/ either (that directory is empty, and git worktree list has only the single line for the main repo), but in the branch names: five branches with the claude/ prefix, and once two were open at the same time and eventually merged into each other.

So these boundaries below — I originally thought I was drawing them for people, but looking back, they’re mostly for “me and the several sessions I spun up myself.” Sessions share no memory whatsoever, which is worse than dealing with a colleague.

Rule files split

One team repo had fairly complete conventions — a prohibition list, version differences, commit rules — but all of it lived under one AI tool’s rules directory, and all of it was set to always apply. The problem: the other tool doesn’t read that directory at all; on its side there’s just one line of import plus the generic prompt injected by the scaffold. The two AIs have completely different context — one knows “there are other production projects on this machine,” the other knows nothing…

My own repo has a ready-made cautionary example too: acg-site maintains three sets of convention files at once, and I only updated one. The version numbers and command descriptions in the other two sat frozen at their old values — I tallied up the gory details in the post about convention files, so I won’t repeat it here.

The conclusion is blunt: keep exactly one source of truth, and point everything else at it with import. The CLAUDE.md in vns-next and thdl are each a single line; the content all lives in AGENTS.md. Change one place and both sides take effect at once — no room for drift.

But that trick only solves “one person maintaining two copies.” The multi-AI scenario adds another layer: the other tool simply doesn’t read the file you’re pointing at. So what really needs confirming isn’t “are the rules complete,” but “what actually got loaded into each agent at startup” — and there’s no shortcut; you have to try it tool by tool.

Boundary one: don’t touch anything else on the machine

The team’s machine that runs the agents has several projects running at once, so we wrote a dedicated “production environment safety constraints” rule and set it to always apply. The prohibitions go roughly like this:

  • Don’t kill processes by port number
  • No batch process-killing commands like killall / pkill
  • Don’t modify anything under system-level config directories
  • Don’t sudo any command
  • Port conflicts get reported to a human only, never handled by the agent itself
  • Database operations are limited to this project’s own database
  • Check the blast radius before acting; if in doubt, ask first

I now copy these onto every machine that has an agent installed… When an agent sees a port is taken, its first instinct is to kill the process holding it, with no idea it’s the neighboring project’s production service.

Boundary two: don’t write APIs from memory

This pitfall is far too common — the API in the training data and the version in your package.json are not remotely the same thing… I’ve given examples of how to turn version differences into ❌→✅ comparison tables in two other posts, so here I’ll only cover the half that matters more for collaboration.

In that rules file the comparison table actually comes last; what opens it is a three-step SOP: first resolve-library-id, then query-docs, write code based on what the docs say, and spell out which scenarios “require checking the docs.” Process first, error examples second — that order matters: error examples are finite and expire, while process can cover the libraries you didn’t anticipate. You can’t list every library’s pitfalls for everyone.

There’s also a lazier route: the newer scaffolds inject a block into the repo themselves — "don’t trust your training data, go read the docs in node_modules" — complete with BEGIN/END markers, plainly meant for tools to overwrite. Nobody has to write the convention, maintenance cost drops to zero; take the freebies where you can~

Conversely, prohibitions need reasons. In the docs/ARCHITECTURE.md:115 of that repo I mentioned earlier, tucked into the tech stack table, there’s a line: client types are generated from the backend OpenAPI, do not introduce tRPC/Prisma, and the reason follows right after — the backend is a REST service in Rust axum, so a JS backend contract simply doesn’t exist. Those two are the default answers in the TS ecosystem, and an AI will install them without thinking; writing only “tRPC is forbidden” won’t stop it, because the next session can always decide the rule is outdated. With the reason attached, it’s not a commandment but a factual judgment. The same document’s :148 even limits the mirror’s permissions: the other side mirrors the DTOs with pydantic “for submission only, do not persist the structure” — a copy is allowed, but the copy must never become the source of truth.

Boundary three: large AI changes must be humanly reviewable

(This is part 1 of 3 of the same post.)

In another team’s repo, the PR template has a line of comment at the top that goes something like this: in scenarios where multiple humans work alongside AI, the PR is the only human gate before bad code reaches production, so please fill out the checklist honestly. Two items in the self-test checklist are aimed squarely at AI: one requires controlling PR size and splitting up large changes, the reason being that a human has to be able to read it in a few minutes; the other requires the submitter to have gone through the AI-written parts line by line and to own them.

Those two items are more useful than any lint rule. Lint only catches things at the syntax level; it can’t catch logic that runs fine but is wrong…

Gates in pre-commit or in CI

That same repo’s choice is to run the full type check + full unit tests in pre-commit. The reasoning is written in a comment in the config: CI only triggers when merging to main, while the staging branch is push-only with no PRs, so if you relied on CI alone the breakage wouldn’t surface until merge…

One detail worth copying—the type check is whole-project, and you can’t just feed it a single file, so the hook returns a fixed command and simply ignores the matched filenames. There’s also a secret-scanning layer on top: local scans the staged changes, CI scans the whole repo.

Comparing against a few of my own repos, the gate strength varies wildly:

ProjectGateStrength
acg-siteCI seven steps (install deps → db:generate → Biome format check → ESLint → tsc → unit tests → build) + pre-commit running lint-staged★★★★★
bbpslint added --max-warnings 0, but typecheck wasn’t added until a month later★★
vns-nextJust one "check": "biome check && tsc --noEmit", no CI, no hook★★
acgn-flowDoesn’t even have a typecheck script—the only static check is eslint .★

The bbps case illustrates it best: lint was already there in 2025-11, --max-warnings 0 wasn’t added until 2026-02, and typecheck and check didn’t make it into package.json until 1d953fe9 a month later… The very day they were added, a type problem got caught—the third-party CSS import in comments-section.tsx that has no type declaration needed an inline comment exemption on the spot.

That repo also has a gate I hadn’t thought of, one that guards cross-language contracts specifically: CI re-runs code generation and then diffs the result against what’s committed.

- run: pnpm gen:client
- name: Fail if OpenAPI/TS client drifted from committed source
  run: git diff --exit-code apps/api/openapi.json packages/api-client/src/schema.d.ts

One byte off and it’s red. docs/ARCHITECTURE.md:176 writing “change the backend = change one place, clients update automatically” is a principle; these two lines are what make it impossible to violate. Same playbook as bbps: the commit that introduced this gate also says “restore a few endpoints that had drifted earlier”—and it caught pre-existing drift the moment it shipped.

By the way, here’s how Biome and ESLint split duties without stepping on each other: biome.json:20-25 in acg-site turns off the linter and assist entirely, so Biome handles formatting only and all linting goes to ESLint; CLAUDE.md:109 spells this out too, so that next time the AI doesn’t see Biome and go turn the linter back on~

28 minutes after the blow-up, the lessons became a script

This is the most beautiful passage I saw in that repo. That afternoon the backend crashed first: a comment structure referencing itself (replies: Vec<CommentOut>), perfectly legal in Rust’s type system, with cargo build, clippy -D warnings, and cargo test all green, but at startup generating the OpenAPI spec recursed infinitely, overflowed the stack, and entered a crash loop, going from 502 all the way to the whole site returning 500—a purely documentation-layer thing took down production. Nine minutes later, the second one: a newly added top-level page wasn’t in the middleware whitelist and got treated as a localized route by i18n, causing a runtime 404, while next build showed the route existed and CI was all green.

Nineteen minutes after that, a third commit turned these two lessons into two smoke tests in the deploy script, 13 lines of shell:

# ①:真跑一次二进制,拦「build 全绿但启动就崩」
SQLX_OFFLINE=true timeout 30 target/release/api --openapi >/dev/null 2>&1 \
  || { echo "✗ 冒烟失败——中止部署,未传镜像"; exit 1; }

# ②:顶层页必须在 middleware 白名单里,否则运行时 404
for _d in apps/web/src/app/*/; do
  _n=$(basename "$_d"); case "$_n" in "[locale]"|dash) continue ;; esac
  [ -f "${_d}page.tsx" ] || continue
  grep -q "\"/$_n\"" apps/web/src/middleware.ts || { echo "✗ /$_n 未放行"; exit 1; }
done

Crude as dirt—one runs the binary, the other greps—but it’s second-scale, deterministic, needs no service started or database connection, and the interception point is before the image is pushed: a bad artifact never even leaves the local machine. All those tsc, clippy, and lint layers are more and more formal, but the two times the site actually got taken down, it was these thirteen lines that stopped it.

How to review AI output

Two examples from over there left a deep impression. One was a deliberately adversarial review after a big AI rewrite, which in one go surfaced a dozen confirmed defects: server-side request forgery, where a configurable address got fetched directly by the server; a bulk import that wasn’t atomic, so failed records were counted as successes too; a data dashboard that mixed rolling 168-hour windows with calendar-day seven-day windows; cache writes that weren’t tagged, so later reads got stale data. Not a single one of these could tsc or lint catch.

Another: a state value was never written by any code, so a multi-step progress bar could never reach the final step—it rendered four circles, but the state advanced at most to the third. Querying production for the number of rows using that value returned 0.

There was also a rather rare one: the requirement was barely described, so the AI made a defensive interpretation and even wrote in the commit message, “if later specs don’t match this interpretation, this change amounts to an unrelated action.” That’s rarer than getting it right~

There are also cases in my own repos that were rescued by human review. In acg-site's c2193c3: a single PR changed the local-dev fallback port from :3000 to :4000, but the other 8 occurrences of .env.example within the same PR still said :3000. The verdict was “dirty changes committed along the way”; reverting kept the whole repo consistent.

The repo where AI accounts for 80% does it more carefully: multiple perspectives independently review the same code, and the output is a numbered, graded list (H1/M1/L1). The conclusion isn’t binary pass-or-fail either; there’s a go-with-notes tier—mergeable, but these items need follow-up. What it catches are all timing issues that lint can’t reach: check-then-write races overwriting each other under concurrency, effect re-runs clobbering unsaved edits, optimistic updates failing without rollback.

The flip side is right here too: this whole process can’t be found anywhere in the repo—no persistent rules file, no CI job, no agent definition. It lives only in a few commit messages. No matter how rigidly structured they are, how many “Lesson:” sections and reproduction commands they include, the next session only gets them if it happens to dig through git log.

A few side effects of parallel worktrees

In thdl, git worktree list shows three lines: the main repo is on main, and the other two are .claude/worktrees/heuristic-bardeen-567381 and .claude/worktrees/sweet-poincare-f3b7bc. The random-background feature was built in one of them. Handy, but there are three side effects.

A worktree is a full checkout, so lint and formatters will scan it

acg-site's eslint.config.mjs:22-25 just adds it to the ignore list:

// IDE local history
".history/**",
// Claude Code worktrees
".claude/**",

biome.json's files.includes excludes it the same way. Without that, running lint once means scanning the same code twice…

Multiple lockfiles make the bundler pick the wrong workspace root (worktrees are only one cause)

bbps’s 449d72b3 added turbopack: { root: process.cwd() } to next.config.ts, with the comment “avoid Turbopack root inference warnings when there are multiple lockfiles”—at that point it had nothing to do with worktrees. home’s 54b4590 hit the same class of problem but in the opposite direction: it had to stop Next from inferring the workspace root from a pnpm-lock.yaml in the parent directory. Worktrees are just one more source.

Easy to commit by mistake

home once committed a subdirectory under .claude/worktrees/ as a gitlink, dragging in 12 .history/ files and a .env along with it—a classic byproduct of git add -A, with not a word about it in the commit message. Later .history was added to .gitignore, but gitignore doesn’t untrack already-tracked files, so that batch is still sitting in the repo to this day. What the hell—I tell myself every time not to use -A…

So remember to add worktree directories to .gitignore and the various ignore lists at the same time; miss one and it’s a trap~

In the end, none of this is a code problem—it’s all about who reads what and who can touch what. No matter how many tools there are, these two things still have to be decided by a human~