From merge-conflict-tool
Carefully resolve git merge, rebase, or cherry-pick conflicts. Tiers the merge by scope, clusters related conflicts, then uses paired defender subagents per cluster to evaluate both sides without bias toward "ours." Frontend conflicts always receive extra care including mandatory visual verification. Triggers when git reports CONFLICT, when you land in a halted merge/rebase/cherry-pick state, or when the user asks to "fix conflicts" / "resolve merge conflicts" / "finish this merge".
How this skill is triggered — by the user, by Claude, or both
Slash command
/merge-conflict-tool:merge-conflict-toolThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Resolve git conflicts with a scalpel, not an axe — preserve both sides' intent, scale caution with scope, treat frontend changes with extra care.
Resolve git conflicts with a scalpel, not an axe — preserve both sides' intent, scale caution with scope, treat frontend changes with extra care.
The naive failure mode is to favor "ours" because it's the side you've been working on, and silently drop "theirs" to make the markers go away. That's fast and almost always wrong. Both sides represent real engineering work; picking one means deleting the other's contribution, often without realizing it.
Mechanism:
Auto-invoke when:
git merge, git rebase, git pull, or git cherry-pick reports CONFLICT and halts..git/MERGE_HEAD, .git/rebase-merge/, .git/rebase-apply/, or .git/CHERRY_PICK_HEAD already present.Do NOT use this skill when the user has explicitly said to take one side ("just take theirs everywhere," "discard my changes and take main"). Honor the override; state once that you're skipping the careful-resolution mode.
ls -d .git/MERGE_HEAD .git/CHERRY_PICK_HEAD .git/rebase-merge .git/rebase-apply 2>/dev/null
| Marker | Operation | Continue | Abort |
|---|---|---|---|
MERGE_HEAD | merge | git merge --continue | git merge --abort |
rebase-merge/ or rebase-apply/ | rebase | git rebase --continue | git rebase --abort |
CHERRY_PICK_HEAD | cherry-pick | git cherry-pick --continue | git cherry-pick --abort |
Rebase reverses ours/theirs. During a rebase, HEAD is the upstream you're rebasing onto, and the commit being applied is the "incoming" side. Git's index uses :2: for HEAD and :3: for the commit being applied — opposite of the merge convention from the user's perspective. Throughout this skill the labels refer to git's index numbers (:2: = current tree = HEAD, :3: = incoming change). For merges this matches "ours/theirs"; for rebase it's flipped. Always speak to the user in terms of which branch each side represents, not abstract "ours/theirs."
git status --porcelain
git diff --name-only --diff-filter=U
git rev-parse HEAD MERGE_HEAD CHERRY_PICK_HEAD 2>/dev/null
git log --oneline -1 HEAD
git log --oneline -1 MERGE_HEAD 2>/dev/null || git log --oneline -1 CHERRY_PICK_HEAD 2>/dev/null
MB=$(git merge-base HEAD MERGE_HEAD 2>/dev/null)
git rev-list --count HEAD ^MERGE_HEAD # commits unique to current side
git rev-list --count MERGE_HEAD ^HEAD # commits unique to incoming side
git log -1 --format="%ai (%ar)" "$MB" # merge base age
git diff --name-only "$MB"...HEAD | wc -l # files current side touched
git diff --name-only "$MB"...MERGE_HEAD | wc -l # files incoming side touched
# Per-subsystem integration overlap
for dir in $(git diff --name-only "$MB"...MERGE_HEAD | awk -F/ '{print $1}' | sort -u); do
ours=$(git diff --name-only "$MB"...HEAD -- "$dir" | wc -l)
theirs=$(git diff --name-only "$MB"...MERGE_HEAD -- "$dir" | wc -l)
echo "$dir: ours=$ours theirs=$theirs"
done
Group conflicts by status code:
| Code | Type | Default approach |
|---|---|---|
UU | both modified | Defender pair (default for non-trivial) |
AA | both added | Often migrations or generated files — see Step 7 |
DD | both deleted | Confirm intent, git rm |
UD / DU | modify/delete | Stop and ask — semantic disagreement |
AU / UA | one added, the other treats as modified | Rare — investigate |
Note any frontend files in the conflict set — they get extra care regardless of size (Step 3). Frontend = any file whose output renders to a screen and whose breakage is hard to catch with typecheck or unit tests: web component files (*.tsx, *.jsx, *.vue, *.svelte, *.astro), *.ts/*.js under UI directories, *.css/*.scss, *.html, mobile UI (SwiftUI views, Jetpack Compose *.kt, Flutter *.dart), desktop UI (XAML, QML), route definitions, build/theme config, and component-catalog files (Storybook, Histoire).
| Tier | Signal | Strategy |
|---|---|---|
| 0 | ≤3 conflicts, 1 subsystem, ≤1 week base age, no frontend, no risky surfaces | Standard workflow per file; defender pair on anything not truly mechanical. |
| 1 | 4–10 conflicts, ≤2 subsystems, ≤1 month base age | Standard workflow. Cluster lightly. |
| 2 | 11–25 conflicts, OR 2+ subsystems, OR 1–3 month base age | Cluster aggressively. Worktree isolation. Stage-and-verify per cluster. |
| 3 | >25 conflicts, OR many subsystems, OR >3 months base age, OR severely asymmetric (one side 10×+ commits) | HALT. Surface scope to user, recommend alternatives (smaller-batch merges, rebase-to-flatten, pair-resolve with the other branch's author) BEFORE resolving anything. Only proceed with explicit user approval. |
Risky-surface modifier (bumps tier +1): auth/authz, crypto, secrets handling, billing, migration logic, build/CI config, deployment manifests, infrastructure-as-code.
Integration-overlap modifier (bumps tier +1): if any subsystem main touched has the current side touching > 5× more files than main did in that subsystem, OR > 30% of subsystem total. Captures the case where textual conflict count is low but our work depends heavily on areas main reshaped — silent breakage risk.
Print the tier and reasoning to the user. If Tier 3, halt and wait for approval.
Group conflicted files by:
grep -l "<basename>" <other-conflicted-files>)git log --oneline --name-only HEAD ^MERGE_HEAD -- <file>)Each cluster gets ONE defender pair seeing ALL files in it — preserves cross-file reasoning. When in doubt, merge clusters — over-large defender pair costs slightly more reading; missed cross-file relationships cost silent breakage.
After resolving each cluster (Step 5), grep for references to its files in unresolved clusters' files; any match = missed clustering, merge and re-pair.
Do the merge in a dedicated worktree rather than directly on the working branch. Workspace-level isolation lets you run full verification, dev server, and browser checks without disturbing the original — and abandon cleanly if it goes sideways. Not redundant with git merge --abort / git reset --hard ORIG_HEAD — those handle ref recovery; worktrees add filesystem isolation.
git worktree add /path/to/merge-attempt-<branch>-<date> <current-branch>
cd /path/to/merge-attempt-<branch>-<date>
Perform Steps 3–9 inside the merge-attempt worktree. On success: push from the worktree, then tear down. On failure: just tear down — the original branch is pristine.
Tier 0/1: skip — in-place merge with git merge --abort as the safety net is sufficient.
git status shows auto-merged files as M (staged-modified), distinct from UU. Two cases:
Distinguish via git diff --name-only "$MB"...HEAD -- <file> (empty = current side untouched = main-only).
Frontend files: always complex (UU or both-sides auto-merge). Frontend bugs are silent — typecheck and unit tests rarely catch a missing dep array, a Tailwind class war, a dropped a11y attribute, or a context provider that lost a wrapper.
Risky surfaces: always complex.
Truly mechanical (skip pair, see Step 6) — only when ALL FOUR hold AND you post the justification:
Justification format (post to user verbatim):
Skipping defender pair on cluster
<name>: (1) no frontend/risky —<files>; (2) purely additive —<description>; (3) one-sentence resolution —<sentence>; (4)<N>/<N>lines,<total>total.
If you can't fill all four with concrete content, dispatch the pair.
Everything else: complex. Defender pair via Step 4.
Both subagents are spawned in a single message with two parallel Agent calls. Each receives only its own side. Neither sees the other's analysis until you (the parent) synthesize.
# For each file in the cluster:
git show :2:<file> > /tmp/mc-current-<basename>
git show :3:<file> > /tmp/mc-incoming-<basename>
git show :1:<file> > /tmp/mc-base-<basename>
git log -p --no-merges HEAD ^MERGE_HEAD -- <file> > /tmp/mc-current-log-<basename>
git log -p --no-merges MERGE_HEAD ^HEAD -- <file> > /tmp/mc-incoming-log-<basename>
# Orienting summaries — cheap on small files, save substantial wall-clock on big ones:
git log --oneline HEAD ^MERGE_HEAD -- <file> > /tmp/mc-current-msgs-<basename>
git log --oneline MERGE_HEAD ^HEAD -- <file> > /tmp/mc-incoming-msgs-<basename>
git log --stat --no-merges HEAD ^MERGE_HEAD -- <file> > /tmp/mc-current-stat-<basename>
git log --stat --no-merges MERGE_HEAD ^HEAD -- <file> > /tmp/mc-incoming-stat-<basename>
You are analyzing one side of a git conflict cluster. The cluster contains:
<list>. You will see only this side's content and history. Do NOT speculate about what the other side did.Your job: articulate what this side accomplishes across the cluster, and what would be lost if it were dropped wholesale.
Inputs (read in this order):
- Commit-subject synopsis (READ FIRST):
/tmp/mc-current-msgs-<basename>— commit subjects often encode themes/phases. Build a mental ToC before diving in.- Per-commit stat summary:
/tmp/mc-current-stat-<basename>— which line ranges and files moved most.- This side's version of each file (no markers):
/tmp/mc-current-<basename>for each file in the cluster.- Conflicted file with markers (line-number context): the actual paths.
- Full diff log:
/tmp/mc-current-log-<basename>. For files where this is large (>50 KB), do NOT linearly scan — use synopsis + stat to identify relevant commits, read those selectively.[Frontend cluster] Additionally read: components that import any cluster file (
grep -rl "from.*<cluster-file>" src/); referenced hooks/contexts; related test files.Return a structured report with EVIDENCE for every claim (concise — under 600 words):
- Intent. What is this side trying to do? (1–3 sentences)
- Mechanism. Cite file:line for each. Quote exact lines. No "around line N."
- Loss if dropped. Name the BEHAVIOR in user/system-facing terms. Cite lines that implement it.
- Hard requirements. Lines/expressions that MUST appear. Quote verbatim with file:line.
- Negotiable parts. What could be reformulated/absorbed.
- External dependencies. Show the grep command AND results (or "no results"). Format:
I ran "grep -rn '<symbol>' --include='*.py' ." and found <paths>.- [Frontend only] UX-affecting changes. Visual/behavioral/a11y differences with
file:line.- What I did NOT verify. Honest list. MANDATORY — empty list triggers re-spawn.
Do not propose a final resolution. Do not consider the other side. Just defend this one.
Symmetric prompt with mc-incoming-* inputs and the symmetric framing. Spawn both in one message with two parallel Agent calls.
Categorize every hunk:
Edits via Edit (not Write). Re-read each file after — confirm no stranded markers, no orphaned references, no half-merged structure (an if from one side and an else from the other that don't compose; a hook called conditionally; a JSX element with an opener from one side and a closer from the other).
Only after the Step 3 four-slot justification was posted. Common cases: union of imports, union of dict/list entries, comment merges (take the more informative one), whitespace match.
If a "mechanical" conflict reveals hidden semantics during edit (imports shadowing each other, conflicting dict keys), promote to complex and dispatch a pair.
These categories recur across every stack. Match the file's category, then apply the pattern. Specific commands differ per ecosystem; the resolution shape doesn't.
Examples: package-lock.json, yarn.lock, pnpm-lock.yaml, Cargo.lock, poetry.lock, Pipfile.lock, Gemfile.lock, go.sum, composer.lock, mix.lock, pubspec.lock.
git checkout --theirs <lockfile>
<package-manager> install # regenerate from merged manifest
git add <lockfile>
This is the one legitimate use of --theirs on a content conflict — the lockfile is derived from the manifest, so any textual merge is wrong by construction.
Examples: package.json, requirements.txt, pyproject.toml, Cargo.toml, Gemfile, go.mod, pom.xml, build.gradle, composer.json, mix.exs, pubspec.yaml.
Examples: OpenAPI specs, GraphQL schema files, type stubs (.d.ts, generated .pyi), protobuf-generated code, ORM-derived schemas, Storybook outputs, static site exports, CSS-in-JS extracted bundles, Terraform plan outputs.
Never hand-merge. Resolve the source-of-truth files first, then regenerate the derivative against the merged source. If both sides regenerated separately, both regenerations are stale — discard them and regenerate fresh.
Applies to Django, Rails ActiveRecord, Alembic, Flyway, Phoenix Ecto, golang-migrate, sqlx, Knex, Diesel, Liquibase, dbmate, etc. Four failure modes:
A. Both sides created independent migrations (non-overlapping changes that just collided on numbering): rename incoming to come after current; update its dependencies / down_revision / parent pointer to the latest current-side migration; verify with the framework's consistency check (e.g. makemigrations --check, db:migrate:status, alembic check).
B. Both sides edited the same source state AND each generated a migration (e.g. both edited the same model and ran the migration generator): the migrations are downstream of incompatible source state. Resolve the source UU via defender pair, then git rm BOTH new migrations and regenerate ONE coherent migration from the merged source. Read it manually before committing.
C. Data migration in conflict (any migration that runs code/SQL against existing rows, not just schema DDL): always defender-pair. Check whether it uses a historical/snapshot pattern (safe under reordering) or a current-state import (fragile). Run migrate --plan (or framework equivalent) and read it. Halt and ask if uncertain — data migrations break behavior silently.
D. Squashed / collapsed migrations (e.g. Django's replaces = [...], Rails schema dumps, Alembic merge revisions): HALT and ask the user. Squashing doesn't autonomously merge with manually-renumbered conflicts.
After any renumbering, search the whole repo for references to old migration names — cross-module dependencies break silently and won't fail the framework's own consistency check.
Examples: tsconfig.json, webpack.config.* / vite.config.* / next.config.*, pyproject.toml, pom.xml / build.gradle, Makefile / justfile, Dockerfile / docker-compose.yml, .github/workflows/*.yml, application settings (settings.py, application.yml, appsettings.json, .env.example), Terraform / Pulumi / CDK config.
Always defender pair. Config encodes intent; silent drops cause runtime errors that don't surface until deploy or first request.
Examples: urls.py, routes.rb, Next.js app/ or pages/, React Router config, Express route registration, Flask / FastAPI router includes, Phoenix router.ex, gRPC service definitions, GraphQL resolver maps.
If route definitions changed on either side, manually trace each route after merge — both sides may have added routes the other isn't aware of, and middleware/guard ordering can shift behavior silently.
The frontend extra-care from earlier steps applies regardless of framework. Concretely:
setup() order, Svelte's reactive declarations, SwiftUI's @State/@Binding and Compose remember* declarations. Order matters and often isn't caught at build time.useEffect/useMemo/useCallback deps in React, watch/watchEffect in Vue, $: in Svelte 4 / $effect in Svelte 5 — must still match captured closures after merge.flex + block); CSS-in-JS theme tokens still need to resolve; specificity wars.Default to defender pair. Cost of an unnecessary pair is minutes; cost of a missed semantic conflict is silent breakage in production.
For Tier 2/3 with cluster-level checkpoints, run verification per cluster before git add-ing that cluster's files.
python -m py_compile <file>, TypeScript tsc --noEmit -p <tsconfig> (project-scoped, not whole monorepo), Rust cargo check, Go go build ./... / go vet, Swift swiftc -parse, .NET dotnet build --no-restore.Find tests that exercise the conflicted modules — grep test directories for imports of those modules using your language's import syntax (e.g. Python grep -rl "from <module>\|import <module>" tests/, JS/TS grep -rl "from .*<module>" over test directories, Go _test.go files referencing the package, Ruby spec/ files requiring it).
Run those. Do NOT run the full suite by default — slow, dilutes signal, makes you tolerant of unrelated failures. Targeted tests fail loudly when your resolution is wrong.
makemigrations --check, Alembic check, Rails db:migrate:status, Knex migrate:status) — must report no pending changes. If it does, you're in Scenario B (Step 7) — regenerate, don't paper over.migrate --plan equivalent — review forward plan for ordering surprises, especially around data migrations.npm run build (or your project's build script). Mandatory — typecheck alone is insufficient.
Required for any frontend file in the merge result with (a) UU resolved on this side, OR (b) both-sides auto-merge, OR (c) main-only auto-merge of a file consumed/imported by code on this side. Skip only when main-only auto-merged frontend files have no integration with this side's frontend code.
browser_navigate to affected routesbrowser_snapshot for accessibility treebrowser_take_screenshot for visual diffbrowser_console_messages for runtime errorsIf Playwright is unavailable, tell the user explicitly and ask them to verify in browser before continuing.
If verification surfaces failures, do not assume they are merge-induced. Branches at scale carry pre-existing debt (failing tests using retired types, missing dev dependencies, lint warnings) that the merge surfaces because verification runs in a clean environment.
Reactive baseline check:
git stash your merge state.The single biggest source of friction in merge resolution is mistaking pre-existing branch debt for merge breakage. Always run the baseline check before treating verification failures as merge problems.
If verification fails AND baseline confirms merge-induced: do NOT continue. Back to Step 5.
If verification fails but baseline confirms pre-existing: continue, document, surface. Don't fix pre-existing debt as part of the merge unless the user explicitly asks (out of scope, expanding scope risks more issues).
git status # confirm no UU/AA/UD/DU
git diff --check # whitespace + stranded markers
git diff --cached # last scan of what's about to commit
git merge --continue # for merges
# or: git rebase --continue # for rebases
# or: git cherry-pick --continue
For merges: let git open the editor for the commit message OR pre-write with -m summarizing what was reconciled. Don't --no-edit blindly — the default merge message rarely captures what was reconciled.
For rebases / cherry-picks: original commit messages are reused.
After: git log --oneline -5 and tell the user what landed plus a one-line summary per cluster.
For Tier 0/1: push directly to feature branch — git push origin HEAD:<feature-branch>.
For Tier 2/3 OR if verification surfaced any pre-existing debt or noise: push to a NEW backup branch FIRST, then fast-forward the feature branch:
git push -u origin HEAD:merge-attempt-<branch>-<YYYYMMDD>
git push origin HEAD:<feature-branch>
The backup branch lets reviewers inspect the merge state independently before it lands on the production-track branch — and gives a clean rollback point if the merge needs to be reverted later.
Halt and confirm before proceeding when:
UD / DU (modify/delete) conflict — always a deliberate human call.git checkout --ours/--theirs on content conflicts — silently discards the other side's work. (Lockfiles excepted.)git merge --abort as a panic button — only after surfacing state and confirming with user.npx claudepluginhub dawsonl1/merge-conflict-tool --plugin merge-conflict-toolCreates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.