with one click
local-merge
// Use when merging a branch to main without touching the primary worktree directly, when /ship or /reflect needs to integrate work, or when autonomous mode blocks direct main commits
// Use when merging a branch to main without touching the primary worktree directly, when /ship or /reflect needs to integrate work, or when autonomous mode blocks direct main commits
[HINT] Download the complete skill directory including SKILL.md and all related files
| name | local-merge |
| description | Use when merging a branch to main without touching the primary worktree directly, when /ship or /reflect needs to integrate work, or when autonomous mode blocks direct main commits |
Merge a branch into a target branch via a disposable shallow clone, then propagate to the primary worktree non-destructively. Defaults to main if no target specified.
| Input | Required | Default | Example |
|---|---|---|---|
BRANCH | yes | — | feat/local-merge-skill |
TARGET | no | main | develop, release/v2 |
PRIMARY | no | First line of git worktree list | /Users/you/projects/repo |
MESSAGE | no | merge: $BRANCH into $TARGET | merge: feat/auth into main |
All commands are explicit. No reasoning required.
git fetch origin "$TARGET" "$BRANCH"
DEPTH=$(( $(git rev-list --count origin/"$TARGET"..origin/"$BRANCH") + 10 ))
Validate inputs before shell interpolation — branch names must match git's allowed character set:
Run this block as a unit (e.g. bash -c '...' or paste the whole fence) — NOT line-by-line. The conflict-detection guard depends on script flow control. Pasting the merge line alone re-creates the silent-failure bug this block is engineered to prevent.
set -euo pipefail
# Validate branch names (reject shell metacharacters)
case "$BRANCH$TARGET" in *['$`\!']*) echo "Invalid branch name"; exit 1 ;; esac
MERGE_DIR="${CLAUDE_SESSION_DIR:-$TMPDIR}/local-merge"
[ -d "$MERGE_DIR" ] && rm -rf "$MERGE_DIR"
git clone --depth "$DEPTH" --branch "$TARGET" "$(git remote get-url origin)" "$MERGE_DIR"
git -C "$MERGE_DIR" fetch origin "$BRANCH"
# Conflict detection MUST be explicit. `set -e` does NOT abort when a command
# is the LHS of `&&`, so a silent conflict followed by `git push` reports
# "Everything up-to-date" and the script exits 0 — masking the failure.
# Use `if !` to capture the merge exit code unambiguously.
if ! git -C "$MERGE_DIR" merge FETCH_HEAD -m "$MESSAGE" --no-edit; then
echo "MERGE-CONFLICT in $MERGE_DIR — aborting Phase 1"
git -C "$MERGE_DIR" status --short
git -C "$MERGE_DIR" merge --abort 2>/dev/null || true
rm -rf "$MERGE_DIR"
exit 1
fi
git -C "$MERGE_DIR" push
git -C "$MERGE_DIR" pull --rebase
git -C "$MERGE_DIR" push
Max 3 retries. After 3 failures, stop and report. Do not force-push.
Always runs after Phase 1, success or failure:
rm -rf "$MERGE_DIR" 2>/dev/null || true
If rm -rf is denied (sandbox, permission prompt), log it and move on — the temp dir is in $TMPDIR and will be cleaned by the OS. Do not block the merge on cleanup failure.
Bring the primary worktree's $TARGET up to date with the newly-pushed remote. Steps 2a-2c are mechanical. Step 2d requires agent judgment. Steps 2e-2g depend on that judgment.
git -C "$PRIMARY" branch --show-current
Not on $TARGET? Stop. Report: "Primary is on <branch>, not $TARGET. Remote updated; local not forwarded." Done.
git -C "$PRIMARY" status --porcelain
If output is non-empty, shelter uncommitted work. Use add -u (tracked files only) to avoid staging sensitive untracked files (.env, credentials):
git -C "$PRIMARY" add -u
git -C "$PRIMARY" commit -m "wip: preserve local state before remote sync"
Record WIP_CREATED=true for step 2g. If clean, skip.
git -C "$PRIMARY" fetch origin "$TARGET"
Run all five commands, then assess using the decision matrix:
git -C "$PRIMARY" rev-list --count HEAD..origin/"$TARGET" # commits behind
git -C "$PRIMARY" rev-list --count origin/"$TARGET"..HEAD # commits ahead
git -C "$PRIMARY" diff HEAD..origin/"$TARGET" --stat # changed files
git -C "$PRIMARY" log HEAD..origin/"$TARGET" --oneline # incoming commits
git -C "$PRIMARY" log origin/"$TARGET"..HEAD --oneline # local-only commits
| Behind | Ahead | Assessment | Action |
|---|---|---|---|
| N > 0 | 0 | Fast-forward | --ff-only (2e) |
| N > 0 | M > 0 | Diverged | Inspect diff for file overlap + semantic risk (2e) |
| 0 | 0 | Already current | Skip, report, done |
| 0 | M > 0 | Local-only commits | Unexpected — escalate to copilot (2f) |
Beyond textual conflicts, assess semantic risk: two agents editing related config in different files can silently break things even when git sees no conflict.
Fast-forward (behind > 0, ahead = 0):
git -C "$PRIMARY" merge --ff-only origin/"$TARGET"
Non-conflicting divergence (different files, low semantic risk):
git -C "$PRIMARY" merge origin/"$TARGET" --no-edit
If merge produces conflicts, abort immediately:
git -C "$PRIMARY" merge --abort
Then escalate to copilot (2f).
Uncertain or risky: do not attempt merge. Go to 2f.
/copilot to enter copilot mode/autonomous to restore previous modeOnly if WIP_CREATED=true:
git -C "$PRIMARY" reset --soft HEAD~1
Preserves all changes in the index and working tree exactly as they were before the skill ran.
pull --rebase, never --force.merge --abort and copilot escalation, never auto-resolution.| Caller | Context |
|---|---|
/ship | CI-down path after review approval |
/reflect | Post-task consolidation to main |
/copilot | Receives escalation from Phase 2f |
/autonomous | Restored after copilot resolves conflict |