Skip to content
Back to skills

Pr Hijack Via Stale Worktree Branch Ref

ASecurity

Diagnose and recover from accidentally overwriting another session's open PR when `git push -u origin <branch>` in a long-lived worktree silently replaces the remote branch ref. Use when ANY of the following appears: (1) you ran `git checkout -b <branch> origin/main` and `git push -u` and git reported `* [new branch]` even though the branch already existed upstream, (2) `gh pr create` then errors with `a pull request for branch "<branch>" into branch "main" already exists` pointing to a PR yo...

  • 3 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 6, 2026
ai-agentspythonrustgobashgitdocumentation

Works with

  • claude code

Security analysis

A100/100

Scanned September 6, 2026

npx -y skills add wan-huiyan/agent-traffic-control --skill pr-hijack-via-stale-worktree-branch-ref --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Pr Hijack Via Stale Worktree Branch Ref?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Pr Hijack Via Stale Worktree Branch Ref
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/wan-huiyan-pr-hijack-via-stale-worktree-branch-ref/badge)](https://www.skillsdirectory.com/skills/wan-huiyan-pr-hijack-via-stale-worktree-branch-ref)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: pr-hijack-via-stale-worktree-branch-ref
description: |
  Diagnose and recover from accidentally overwriting another session's open PR
  when `git push -u origin <branch>` in a long-lived worktree silently
  replaces the remote branch ref. Use when ANY of the following appears:
  (1) you ran `git checkout -b <branch> origin/main` and `git push -u` and
  git reported `* [new branch]` even though the branch already existed
  upstream, (2) `gh pr create` then errors with
  `a pull request for branch "<branch>" into branch "main" already exists`
  pointing to a PR you didn't author, (3) `gh pr view N --json commits` on
  that PR shows YOUR commit instead of the one the title/body describes,
  (4) you discover your worktree's session-start git status banner was stale
  (showed branch X but actual branch was Y). Pattern surfaces when a prior
  session in the SAME worktree opened a PR on a generically-named feature
  branch, then reset the worktree's local refs without cleaning the remote.
  Recovery requires `git push --force-with-lease` — a destructive op
  requiring explicit user authorization. Sister to `git-recover-lost-branch`
  (mechanics of recovering an orphaned commit via reflog) and
  `gh-pr-merge-worktree-checkout-trap` (different worktree-vs-gh gotcha).
author: Claude Code
version: 1.0.0
date: 2026-05-12
disable-model-invocation: true
---

# PR-Hijack via Stale Worktree Branch Ref

## Problem

In a long-lived git worktree (especially `.claude/worktrees/<name>/` patterns
created by a parallel-session harness), a same-named local branch can be
created via `git checkout -b X origin/main` even when branch `X` already
exists upstream with an open PR. Pushing that local branch with
`git push -u origin X` succeeds — git reports `* [new branch]` — and
silently replaces the remote ref. The result:

- The remote branch ref now points at YOUR commit
- The open PR (call it #N) on that branch now lists YOUR commit
- PR #N's title and body still describe the ORIGINAL author's work
- The original author's commit is orphaned on the remote but still
  reachable in YOUR local reflog (because the worktree's reflog captured
  it earlier)
- `gh pr create` fails with `a pull request for branch "X" into "main"
  already exists` and surfaces PR #N — which is now an artefact of two
  unrelated commits

The hijack is silent on both sides: git's push output doesn't warn, gh
doesn't either, and the PR's web view shows a perfectly plausible (but
wrong) state.

## Context / Trigger Conditions

Surfaces when ALL of:

1. You are working in a worktree (typically `.claude/worktrees/<name>/`)
   that has been used by a previous session
2. The session-start git status banner shows a branch name that does NOT
   match `git status -sb` once you actually run it (stale banner)
3. A prior session in the same worktree opened a PR on a generically-named
   feature branch (e.g., `feat/drivers-headline-and-stripe-removal`,
   `feat/refactor-foo`, `fix/bar-bug`)
4. The worktree's local refs were reset between sessions (`git reset
   --hard`, branch deletion, or worktree-recreation) WITHOUT a
   corresponding `git push --force` to clean up the remote
5. You create a local branch by the same generic name

Exact failure-signature triad:

```
# Symptom 1 — push reports new-branch despite upstream existing
$ git push -u origin feat/<name>
* [new branch]        feat/<name> -> feat/<name>

# Symptom 2 — gh pr create rejects with already-exists
$ gh pr create ...
a pull request for branch "feat/<name>" into branch "main" already exists:
https://github.com/<org>/<repo>/pull/N

# Symptom 3 — that PR's commits don't match its title/body
$ gh pr view N --json commits
{ "commits": [{ "oid": "<YOUR-SHA>", "messageHeadline": "<YOUR WORK>" }] }
```

If the gh-pr-view output shows YOUR work and the PR title describes
something entirely different — confirmed hijack.

## Solution

**Stop before doing anything else.** Recovery requires a destructive
force-push to a remote branch you don't own. Ask the user for explicit
authorization first.

### Step 1 — Locate the original SHA via reflog

```bash
git reflog --date=iso | grep -E "<branch-name>|<keyword-from-pr-title>" | head -10
```

The reflog entry will look like:

```
<orig-sha> HEAD@{<timestamp>}: commit: <ORIGINAL PR TITLE>
<base-sha> HEAD@{<timestamp>}: checkout: moving from <old> to <branch>
```

Verify the orphaned commit is still reachable:

```bash
git cat-file -t <orig-sha>   # should print "commit"
git log --oneline <orig-sha> -3
```

If the reflog is empty or doesn't contain the SHA, you may need to:
- `git fsck --unreachable` (orphan-walk)
- Pull the PR's HEAD via `gh pr view N --json headRefOid`

### Step 2 — Stash your work to a fresh branch FIRST

Critical ordering: create the rescue branch BEFORE resetting the current
branch. Otherwise you'll have nothing to push to a new branch.

```bash
git branch <new-branch-name> <YOUR-SHA>   # e.g., feat/<descriptive-new-name>
git branch -v | grep <descriptive>         # verify both branches exist
```

### Step 3 — Reset the hijacked branch back to the original SHA

```bash
git reset --hard <orig-sha>
git log --oneline -2   # confirm HEAD is now at the original commit
```

### Step 4 — Force-with-lease to restore PR #N

```bash
git push --force-with-lease=<branch>:<YOUR-SHA> origin <branch>
```

`--force-with-lease=<branch>:<YOUR-SHA>` says "only push if the remote
ref is currently exactly YOUR-SHA" — which it is, since you just pushed
it. This guards against clobbering anything a third party may have pushed
since.

After this, `gh pr view N --json commits` should show the original SHA
and the PR is fully restored.

### Step 5 — Push your work to the new branch

```bash
git checkout <new-branch-name>
git push -u origin <new-branch-name>
gh pr create --title "..." --body "..."
```

### Step 6 — Document the collision in the new PR

Cross-link to PR #N in the new PR's "branch history" or "notes for
reviewer" section. Mention the two PRs touch the same page/area but
different code paths. This avoids the reviewer wondering why two PRs on
sister branches exist.

## Verification

After recovery, verify all three sides:

```bash
# Local reflects truth
git log --oneline <original-branch>   # → original SHA on top
git log --oneline <new-branch>        # → your SHA on top

# Remote reflects truth
gh pr view N --json title,commits | python3 -m json.tool
# title and commits should describe ORIGINAL work

gh pr view <NEW-N> --json title,commits
# title and commits should describe YOUR work

# CI on both PRs should re-run automatically after the force-push
gh pr checks N
gh pr checks <NEW-N>
```

## Prevention

The trigger is **stale local refs in a long-lived worktree**. Two habits
prevent it:

1. **Verify branch state at session start.** When entering a worktree,
   `git status -sb && git log --oneline -3 && git ls-remote origin
   <branch-you-plan-to-create> | head -2`. If `ls-remote` shows the
   branch exists upstream and your local reflog has no record of it,
   pick a different name.

2. **Prefix branch names with session/date.** Generic names like
   `feat/refactor-foo` collide across sessions. `feat/<YYYYMMDD>-foo`,
   `feat/<session-id>-foo`, or worktree-scoped names eliminate the
   collision class entirely. The repos that have this convention —
   session- or date-prefixed synthetic IDs, applied consistently —
   don't see this bug.

If your worktree harness shows a session-start git status banner, **don't
trust it**. The banner is captured at worktree creation and can be stale
after subsequent operations. Always run `git status -sb` first.

## Notes

- This is NOT the same as `stale-base-pr-silently-reverts-upstream-content`
  (that's about merging a stale PR clobbering main's progress). The trigger
  here is the OPPOSITE direction: your local clobbering the remote on push.
- It is also NOT the same as `gh-pr-merge-worktree-checkout-trap` (failed
  merge due to worktree-locks-main). Here the merge isn't involved; the
  collision happens at push time before any merge.
- Once force-with-lease restores PR #N, CI will re-run. If the original
  author's PR was already approved, the re-trigger will require re-approval
  (GitHub behavior). Warn the author.
- If the original branch had multiple commits and only some were captured
  in your reflog, the recovery is partial — pull `gh pr view N --json
  headRefOid` BEFORE resetting to capture the full upstream tip.
- The `git push -u origin <branch>` reporting `* [new branch]` is the
  EARLIEST detectable signal. If you see that message and the branch name
  is generic, immediately `gh pr list --head <branch>` to check for
  pre-existing PRs before continuing.

## Example

Real session (2026-05-12, the-project-repo repo, Opus 4.7):

1. Worktree `review-driver` was created from a session ~12:33Z that
   committed `e4796f17 Fix /drivers chevron: filter NULL raw_value rows`
   to `feat/drivers-headline-and-stripe-removal`, pushed, opened PR #761.
2. That session reverted the local branch ref to `origin/main` but left
   the remote branch + PR alone.
3. The next session entered the worktree, ran `/impeccable critique driver
   page`, decided to fix headline + side-stripes, did `git checkout -b
   feat/drivers-headline-and-stripe-removal origin/main` — collided
   with the upstream ref but git locally treated it as new.
4. Committed `5107c97f` and pushed; got `* [new branch]`.
5. `gh pr create` returned "a pull request for branch X already exists:
   #761". `gh pr view 761` showed PR #761's title was still the chevron
   fix, but the only commit listed was `5107c97f` (the hijacker).
6. Recovery: `git branch feat/drivers-headline-take 5107c97f` →
   `git reset --hard e4796f17` → `git push --force-with-lease=feat/
   drivers-headline-and-stripe-removal:5107c97f origin feat/
   drivers-headline-and-stripe-removal` → `git checkout feat/drivers-
   headline-take` → `git push -u origin feat/drivers-headline-take` →
   `gh pr create` → PR #762. Total recovery time: ~3 minutes once the
   hijack was diagnosed.

## References

- `git push --force-with-lease`:
  https://git-scm.com/docs/git-push#Documentation/git-push.txt---force-with-leaseltrefnamegt
- `gh-pr-merge-worktree-checkout-trap` (sister skill, different
  worktree-vs-gh failure mode)

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.