DEV Community

pponali
pponali

Posted on

Stop Wrestling with Merge Conflicts: Automate the Whole Workflow

It's 4:47 PM on a Friday. You've been coding all week on a feature you're proud of. You open a PR, and GitHub greets you with the one message that turns your stomach:

"This branch has conflicts that must be resolved."

You already know what's waiting. The pom.xml has diverged again. Someone on main bumped a YAML config. There are three Markdown docs with ugly <<<<<<< HEAD markers scattered through them. You're not shipping today.

Every software developer has lived this moment. And most of us keep solving it the same slow, error-prone, manual way — every single time.

This post is about why that's a problem, and how the /resolve-conflicts skill for Claude in Cowork mode fixes it by automating the entire workflow.

The Real Pain Points Behind Merge Conflicts

Merge conflicts feel like a Git problem, but they're really a team coordination problem made visible. Here's what actually makes them brutal:

They always hit at the worst time. You finish a feature after days of work, go to merge, and suddenly you're debugging someone else's schema migration inside your application.yml. Context-switching mid-sprint to resolve config conflicts is a massive cognitive tax.

The same files conflict over and over. In any non-trivial project, you'll find the same suspects in your conflict list week after week: pom.xml, docker-compose.yml, *.properties, localization files, OpenAPI specs. These aren't complex logical conflicts — they're mechanical, repetitive, and boring. Yet they still demand your full attention.

Manual resolution is error-prone under pressure. When you're staring at a wall of ======= markers, it's easy to accidentally keep the wrong version, drop a dependency update, or smash together two configs in a way that silently breaks something. There's no guardrail — just you, your eyes, and your mouse.

The feedback loop is slow. You resolve conflicts, push, wait for CI, find out you introduced a bug in the config you just "fixed," fix it again, push again. The cycle can eat hours.

It doesn't scale with team size. On a small team, conflicts are annoying. On a team of 20+ engineers all merging into main frequently, they're a daily tax on every developer's time.

The Traditional Approach (And Why It Falls Short)

The standard workflow most of us follow looks like this:

git fetch origin
git merge origin/main
# ... open every conflicted file in VS Code ...
# ... manually choose ours / theirs / both ...
git add .
git commit -m "Merge branch 'main' into feature/my-thing"
git push
Enter fullscreen mode Exit fullscreen mode

This works, but it has serious problems:

  • No consistency. Different developers make different choices for the same file types. One dev always keeps pom.xml from main; another always keeps the branch version. The merge history becomes unpredictable.
  • No documentation. The merge commit rarely explains why you resolved conflicts the way you did. Future you (and your teammates) have no idea.
  • No scalability. Every conflict requires a human to sit down and make individual decisions, even for files where the right answer is always the same.

Enter /resolve-conflicts: Automated, Opinionated, Auditable

The /resolve-conflicts skill automates the entire conflict resolution workflow using a shell script (.claude/scripts/resolve-conflicts.sh) with codified heuristics for each file type. Instead of opening conflict markers by hand, you describe what you want and Claude runs the script.

The core idea: most conflicts in most projects follow predictable patterns. Once you document those patterns as code, you never have to think about them again.

Here's what a typical session looks like:

# Step 1 — preview what's going to happen (highly recommended)
.claude/scripts/resolve-conflicts.sh origin/main true

# Step 2 — run for real
.claude/scripts/resolve-conflicts.sh origin/main false
Enter fullscreen mode Exit fullscreen mode

That's it. The script handles stashing, merging, resolving, staging, committing, restoring, pushing, and optionally checking PR status via the gh CLI.

How the Resolution Heuristics Work

The script doesn't guess — it applies explicit, per-file-type rules:

File pattern Strategy Rationale
pom.xml, */pom.xml Keep branch (--ours) Feature branch usually holds newer or WIP dependencies
*.properties Keep branch Branch has local/feature-specific config values
*.yaml / *.yml Keep branch Same reasoning as properties
docker-compose.* Keep branch Preserves the current service topology
*.md Keep both (concatenated) Docs from both sides are usually additive, not contradictory
*.java Keep branch Active feature work lives on the branch
*.js, *.ts, *.tsx Keep branch Same as Java
anything else Keep branch + warning Safe default; flagged for manual review

"Keep branch" means the HEAD side of the merge — your feature branch. The strategy is opinionated by design. If your project has different conventions, you update the script once, and every future merge follows the new rules automatically.

The Markdown strategy is particularly clever: instead of picking one side, it concatenates both. Documentation is almost always additive — two sections from different branches are usually both worth keeping, so the merge result includes both and a human can clean it up later without losing information.

The Dry-Run Is Your Best Friend

Before running for real, always do a dry run:

.claude/scripts/resolve-conflicts.sh origin/main true
Enter fullscreen mode Exit fullscreen mode

The output tells you:

  • Which files have conflicts
  • Which rule the script will apply to each one
  • Any files in the "unknown" bucket (these get --ours by default and a warning logged)

This is the moment to catch surprises. If main bumped a critical version in pom.xml that your branch must absorb, you'll see it here. You can let the script handle everything else and manually fix just that one file afterwards.

Verify, Don't Assume

After the script runs, always check:

# Should show your merge commit
git log --oneline -3

# MUST be empty — any output here means unresolved conflicts
git diff --name-only --diff-filter=U

# Quick overview of what changed
git show HEAD --stat
Enter fullscreen mode Exit fullscreen mode

If your project has a fast smoke test — mvn -q -DskipTests verify, npm run typecheck, docker compose config — run it now. Heuristic resolutions are fast but not infallible.

If you're using the gh CLI:

gh pr view <number> --json mergeable,mergeStateStatus
Enter fullscreen mode Exit fullscreen mode

Note: GitHub takes 1–2 minutes to recompute mergeability after a push. Don't panic if it shows conflicts for a moment after you push.

When NOT to Use the Script

The script is great for mechanical conflicts, but there are cases where you need a human in the loop:

Core business logic in .java / .ts / .js files where both sides have unique, non-overlapping changes. The script will keep the branch version and silently drop main's changes. If both sides implemented different parts of the same feature, you need to merge them manually.

A critical dependency update in pom.xml that your branch must consume. The script keeps your branch version. After it runs, manually edit the file, git add, and git commit --amend --no-edit.

Files in the "unknown" bucket (*.sql, *.proto, *.kt, etc.). The script flags these but defaults to --ours. Open them, check if main's changes matter, resolve manually, then fold into the merge commit:

# After manually editing the file:
git add path/to/file.sql
git commit --amend --no-edit
git push --force-with-lease origin your-branch
Enter fullscreen mode Exit fullscreen mode

The pattern that works best: let the script handle the 80% of mechanical conflicts, then manually patch the 20% that need actual reasoning. You get speed and correctness.

Real-World Payoff

Once your team adopts this workflow, a few things change:

Merge commits become consistent and auditable. The commit message documents which strategy was applied to which files. Six months from now, you can look at a merge commit and understand exactly what happened.

Conflict resolution stops being a senior-developer task. The rules are encoded in a script, not in someone's head. A junior developer can merge safely without needing to ask "should I keep ours or theirs for the YAML?".

Friday afternoon PRs stop being feared. When conflict resolution is one command instead of a half-hour of careful manual editing, you stop dreading the merge.

Getting Started

The skill is built into Claude's Cowork mode. Just describe what you want:

  • "Merge main into my branch and resolve conflicts"
  • "My PR shows conflicts — fix them"
  • "Sync my feature branch with origin/main"

Claude will run the pre-flight checks, do a dry run, summarize what it found, and ask for confirmation before committing and pushing. You're always in control; the automation just handles the mechanical parts.

If you want to run the script directly without Claude, the quick reference is:

# Dry run first — always
.claude/scripts/resolve-conflicts.sh origin/main true

# Then for real
.claude/scripts/resolve-conflicts.sh origin/main false

# Merge a different branch
.claude/scripts/resolve-conflicts.sh origin/feat/payment false
Enter fullscreen mode Exit fullscreen mode

Merge conflicts aren't going away. As long as multiple people work on the same codebase, there will be divergence. But the amount of manual, repetitive work they require is entirely within your control. Codify your team's conventions, automate the mechanics, and save the human judgment for the conflicts that actually need it.

Your future Friday self will thank you.

Top comments (0)