I run multiple AI coding agents (Claude Code, Codex, Cursor Agent, whatever)
in parallel git worktrees on the same repo, and eventually they collide in a
way git merge can't see. This is a small CLI I built to catch that.
See CHANGELOG.md for what's new (v0.2 adds cross-file conflict detection and N-way branch support).
Say Agent A renames a function, changes what it returns, or changes some
assumption about shared state. Meanwhile Agent B, working in a different
worktree with zero visibility into what A is doing, writes new code that
depends on the old behavior. If those two changes touch different lines —
or even different files — git merge doesn't see a conflict. No markers, no
warning, exit code 0. The build's green. It just doesn't work anymore, and
you find out later, the hard way.
conflict-check tries to catch that before you merge. You tell it what each
agent was supposed to be doing, it grabs the diff each agent actually
produced, and it asks an LLM whether the two look like they're making
conflicting assumptions about the same code.
This isn't a made-up example — it's a real run of npm run demo against the
Gemini provider:
- Branch
agent-a-renamerenamesadd()tosum()inmath.js. - Branch
agent-b-add-helperadds a newsumAll()in the same file, in a different part of it, that callsadd()— no idea the rename happened.
Merge them and see what git thinks:
$ git checkout agent-a-rename && git merge agent-b-add-helper
Auto-merging math.js
# clean merge. no conflict markers. exit code 0.Totally clean, as far as git's concerned. Except sumAll() now calls a
function that doesn't exist anymore. Here's what conflict-check check says
about the same two branches, before you merge:
$ conflict-check check --branch-a agent-a-rename --branch-b agent-b-add-helper
Checking "agent-a-rename" (1 file) against "agent-b-add-helper" (1 file)...
Using gemini (gemini-3.6-flash)
Overlapping files: math.js
Checking math.js... CONFLICT
✗ math.js
Agent A renames the `add()` function to `sum()`, while Agent B adds a new
`sumAll()` function that explicitly calls `add()`. If both changes are
merged, `sumAll()` will attempt to call `add()`, which no longer exists,
resulting in a runtime ReferenceError.
Summary:
✗ 1 of 1 overlapping file(s) have a likely semantic conflict.
That's the actual, unedited output. I didn't feed it the word "ReferenceError" anywhere — it figured that out from the two diffs on its own.
The same-file example above is the easy case — both agents touched
math.js, so there was an overlapping file to even look at. The harder,
more common case is when they don't share a file: Agent A renames a
function in one file, and Agent B calls the old name from a completely
different file it added or already had. No overlapping filename, no diff
collision — git merge sees nothing, and neither did earlier versions of
this tool.
conflict-check now parses each branch's diff with a real AST (via
ts-morph) to find functions/methods
that were added, removed, or renamed, then searches the whole repo — not
just the diff — for anything that still calls the old name. Here's that
exact scenario, still from npm run demo:
- Branch
agent-a-renamerenamesadd()tosum()inmath.js. - Branch
agent-b-add-helperadds a brand-new file,batch.js, that importsaddfrommath.jsand calls it — a fileagent-a-renamenever touched, and that didn't exist before either branch started.
$ conflict-check check --branch-a agent-a-rename --branch-b agent-b-add-helper
Checking 2 branches (1 pair): agent-a-rename, agent-b-add-helper
Using gemini (gemini-3.6-flash)
=== "agent-a-rename" ⇄ "agent-b-add-helper" ===
[same-file] 0 file(s) touched by both branches
✓ No files touched by both branches.
[cross-file] renamed/removed symbols still referenced elsewhere
Checking math.js: `add` renamed to `sum`... CONFLICT
CROSS-FILE math.js — `add` renamed to `sum` — from branch "agent-a-rename"
CONFIRMED REFERENCE
[confirmed] batch.js:1 (branch "agent-b-add-helper") — import { add } from "./math.js";
[confirmed] batch.js:4 (branch "agent-b-add-helper") — return numbers.reduce((total, n) => add(total, n), 0);
Branch agent-a-rename renamed `add` to `sum` in math.js, but branch
agent-b-add-helper's new batch.js still imports and calls `add`, which no
longer exists after the rename. Merging both branches will throw at
import/call time.
=== Summary ===
✓ No same-file conflicts across 0 checked file(s).
✗ 1 cross-file conflict(s) with a CONFIRMED reference (real import/scope resolution).
Caught with zero overlapping files. Note the CONFIRMED REFERENCE tag —
that's not decoration. It means the reference was resolved through real
import/scope binding (ts-morph's language service), not just a name that
happens to match. When that can't be resolved (say, both branches touched
the same symbol, or the calling code has no module system to trace through),
check falls back to a plain AST name match instead and tags it
POSSIBLE — NAME MATCH ONLY — still worth a look, but a coincidentally
reused name could slip through there, so treat it as a lead to verify, not
a confirmed diagnosis. Every reported reference — same-file or cross-file,
confirmed or possible — carries its label front and center in the terminal
output, never just tucked away in some internal field.
Not on npm yet, so you'll have to build it yourself:
git clone https://github.com/Cipher-08/conflict-check.git
cd conflict-check
npm install
npm run build
npm linknpm link is what makes the conflict-check command available globally. It
symlinks the built output, so after you pull changes you just need npm run build again — no need to re-link.
If npm link blows up with EACCES, that's your global npm prefix (often
/usr/local/lib on a Mac) not being writable by your user — happens a lot
with a system-installed Node. Don't sudo npm link your way through it;
either fix the prefix permissions or just switch to nvm, which sidesteps
the whole problem. Or skip linking entirely and run it as
node dist/index.js <command> from inside the repo.
Once it's linked, go into whatever repo you actually want to check and run:
conflict-check initconflict-check init— sets up.conflict-check/in the current repo. Prompts for an Anthropic key if it doesn't find one in your env.conflict-check log-task --branch <name> --description "<text>"— jot down what an agent's about to work on. Skip either flag and it'll just ask you interactively.conflict-check snapshot --branch <name> --base <base>— runsgit diff <base>...<branch>and saves it to.conflict-check/snapshots/<branch>.diff, plus a small<branch>.meta.jsonrecording the branch's commit sha (used later for cross-file checking, even if the branch/worktree is gone by the time you runcheck).conflict-check check— the actual point of the tool. Runs the same-file and cross-file checks over every pair of branches you give it, prints a report per pair, then a summary. Exits non-zero on any conflict (confirmed or possible) or if a check couldn't complete, so you can stick it in a pre-merge script. Give it branches any of these ways:--branch-a <name> --branch-b <name>— the original two-branch form, still works exactly as before.--branch <name>— repeat it for as many branches as you're comparing.--branches a,b,c— same thing, comma-separated.
Normal flow looks like this:
conflict-check init
# before each agent starts
conflict-check log-task --branch agent-a-branch --description "Rename add() to sum()"
conflict-check log-task --branch agent-b-branch --description "Add a helper that calls add()"
# once each agent's done
conflict-check snapshot --branch agent-a-branch --base main
conflict-check snapshot --branch agent-b-branch --base main
# before you actually merge
conflict-check check --branch-a agent-a-branch --branch-b agent-b-branchRunning more than two agents at once? Snapshot each one, then check them all together — every pairwise combination gets checked, and conflicts are reported grouped by which pair they're between:
conflict-check check --branches agent-a-branch,agent-b-branch,agent-c-branchAll of this is just JSON and diff files sitting in .conflict-check/ at the
repo root — no database, no server, no account, nothing to sign up for.
Two options, and it picks whichever key you've got set
(GEMINI_API_KEY wins if both happen to be present):
Gemini — free, no credit card needed. Go to aistudio.google.com, sign in, click "Get API key" → "Create API key", copy it:
export GEMINI_API_KEY=AIza...Defaults to gemini-3.6-flash, override with GEMINI_MODEL if you want.
Free tier has rate limits but it's plenty for trying this out.
Anthropic — needs a billed account:
export ANTHROPIC_API_KEY=sk-ant-...Or just let conflict-check init prompt you for it — it'll stash it in
.conflict-check/config.json, which is gitignored. Defaults to
claude-sonnet-5, override with CONFLICT_CHECK_MODEL or a "model" key
in that same config file.
Either way, your key only ever goes to that provider's own API, and only
when you actually run check.
Being upfront about this stuff up front, most important first:
- Cross-file detection only understands JS/TS/JSX/TSX, and only functions, arrow-function/function-expression consts, and class methods. Other languages, or symbols like exported constants/types/React components declared some other way, aren't tracked — a removed/renamed one of those won't be caught by the cross-file check (same-file checking is unaffected; it works on raw diffs regardless of language).
- The "possible (name match)" fallback can still be fooled by a
coincidentally reused name. It only kicks in when the symbol's old
declaration can't be semantically resolved in the corpus (e.g. both
branches touched the same file), and at that point it's back to matching
by name with a same-file-declaration shadow check — better than a raw
grep, but not a guarantee. That's exactly why it's tagged differently in
the output from a
CONFIRMED REFERENCE— treat it as a lead, not a verdict. - Cross-file checking needs
conflict-check snapshotto have run with this version of the tool (it records the branch's commit sha). A snapshot taken before this feature existed just gets skipped for cross-file purposes, with a warning — same-file checking on it still works. Re-runsnapshotto pick it up. - Cross-file checking also needs the branches' commits to still be resolvable in git — reachable from some ref or otherwise not garbage-collected. Deleting a branch outright (not just the worktree) not long after snapshotting it risks losing that.
- It only warns, it never touches your code or does the merge for you.
- No dashboard, no accounts, nothing hosted. Just you, your terminal, one repo at a time.
- Filename matching (same-file check) is dumb string comparison on diff headers, not quote-aware — files with spaces or weird characters in the name (which git escapes in diff output) probably won't match up correctly.
- On a very large repo, the cross-file check parses every tracked JS/TS file to build its search corpus — there's no size cap yet, so expect it to take longer the bigger the repo.
- Resolve bare/aliased imports (
node_modules, tsconfig path aliases, monorepo package references) for cross-file reference finding — right now only relative imports resolve, since the corpus is built purely from git blobs with nonode_modulesaround to resolve into. - Track exported constants, types, and other non-function/method symbols in the cross-file check, not just functions/arrow-functions/methods.
- Maybe a hosted dashboard for teams, eventually, if the local version turns out to actually be useful to people. Not promising anything here.
It's rough, it's a v1, and I built it fast — issues and PRs are welcome. If you find a case where it misses something obvious or flags something that isn't actually a problem, open an issue with the two diffs involved. That's the feedback that'll actually shape what comes next.
npm test runs the test suite (vitest) — mostly the AST symbol-diffing
and cross-file reference resolution logic, including one end-to-end test
that spins up a real temp git repo.