Landing a branch
Take a branch that is already committed and get it merged. commit
ends by reporting hashes "against a clean tree" with an explicit note
that nothing was pushed; this starts exactly there.
Two things here are irreversible or outward-facing — opening the PR and
pushing main — so the procedure gates each one.
1. Preflight
git status --short # must be clean
git branch --show-current # must not be main
git log --oneline origin/main..HEAD
A dirty tree means commit has not finished. Nothing to land means
there is nothing to do — say so rather than opening an empty PR.
Ask whether another session is live in this working tree. Step 7
runs git switch, and one working tree has one HEAD, so the switch is
not scoped to you — it moves the branch under anyone else working here.
If someone is, stop after step 6 and let them finish.
2. Establish the identity before anything outward
This gates every outward action, and it is the step most likely to fail silently.
git config user.name # the includeIf identity for this root
gh api user -q .login # the account gh will actually act as
git remote -v # who owns the repo
Then github-mcp → get_me when that server is loaded, and compare
every login against the repo owner. Use the tool whose account matches.
On this machine gh is folder-scoped (since 2026-09-04): both
shell profiles wrap it to act as the account named by the repo's
user.name, resolved per call through a scoped GH_TOKEN, so a
personal repo gets the personal account and a client root gets the
client one. Two consequences. gh auth status reports the keyring's
active account, not the one the wrapper will use — gh api user is
the honest probe. And the wrapper is a profile function, so a gh run
from a script or hook that skips the profile falls back to the active
account; probe through the same shell the PR command will use.
github-mcp is project scope (.mcp.json), bound to one token, and
absent in any repo that does not declare it. Loaded and matching,
prefer it — it is the identity the repo's own config chose. Absent, a
gh whose confirmed login matches the repo is the right tool, not a
fallback (2026-09-04, a client repo: no MCP, gh confirmed as the
client account, PR opened cleanly). Root CLAUDE.md states the rule;
what it cannot do is make the comparison happen.
Nothing warns you. gh is authenticated, it works, it reports
success — the PR simply appears under the other account. A wrong-account
PR looks identical to a right-account one until someone reads the
author. If no identity matches the repo owner, stop and ask.
3. Push the branch
git push -u origin <branch>
4. Survey what is actually landing
git log --oneline origin/main..HEAD
Read every commit, not just yours. A commit another session made while you were on this branch landed on this branch — that is how one working tree behaves, and those commits ship inside your PR. They are not a problem to fix, but shipping someone else's work inside a PR described entirely as yours is a review problem. Name them in the body (step 5).
Anything that should not ship yet is a stop: say so rather than landing it.
5. Open the PR — through the identity step 2 confirmed
github-mcp → create_pull_request with owner, repo, title,
head, base, body; or, when gh is the confirmed tool,
gh pr create --title … --body-file … from the same shell that was
probed.
The body is the deliverable, and it is the only judgement step in this skill. The diff is already visible; do not restate it. What it owes a reader:
- Why the change exists, and what it decided.
- What was verified, and what was not — including anything believed but unmeasured. A claim's provenance is the part that rots first.
- Reasoning behind any reversal of a previous decision, quoted, so the reversal is auditable rather than silent.
- Disclosure of concurrent-session commits from step 4.
- How it is to be integrated, where the repo has a convention.
No GitHub remote, or no confirmed identity? Stop. An origin that
is not GitHub — a file:// path, another host — means there is no PR
to open; no github-mcp and no gh login that matches the repo
means the same thing for a different reason. Neither is permission to
skip ahead and merge locally. A local merge satisfies the literal words
"merged into main" while discarding review, CI and the PR body, and it
makes any later PR empty because main already contains the commits.
Report what is missing and let the user choose. Reaching for an
unconfirmed gh is never the answer — step 2.
6. Checkpoint — stop here
The gate is the next command that writes to main, not the PR. Stop
before that command whether or not a PR exists: a PR that could not be
opened is a reason to stop sooner, never a reason to carry on.
Report where things stand — the PR URL, or what blocked it — and state exactly what happens next. Then wait.
Everything past this point writes to main. Do not continue on your own
initiative, even when the merge looks routine, and even when the local
half would plainly succeed on its own — this checkpoint is the skill's
whole reason for not being one command.
7. Land by fast-forward
git switch main && git merge --ff-only <branch> && git push origin main
This preserves the exact SHAs, keeps main linear, and adds no merge
commit. GitHub marks the PR merged once its commits are reachable, so
the merge button is never needed.
Why not each alternative:
- The merge button / a merge commit — adds a commit that this repo has never had.
- Squash — collapses the logical split
commitjust built. The whole point of separate commits is that each is independently revertible and citable. Asked for anyway? It is overridable, but not silently — see Constraints. - Rebase merge — rewrites SHAs, and may be disabled outright. Read
the setting rather than assuming it:
gh api repos/<owner>/<repo> --jq '{allow_squash_merge, allow_merge_commit, allow_rebase_merge}'. Unauthenticated, those fields come backnull— so a check without a token proves nothing and a rejection surfaces at merge time. A repo's answer is not durable either: a delete-and-recreate resets all three to GitHub's defaults.
--ff-only fails loudly rather than inventing a merge commit. If it
fails, main has moved: stop, reconcile deliberately, and never reach
for --force.
8. Verify
pull_request_readmethodget→ expectmerged: trueand amerged_bythat matches the identity from step 2.pull_request_readmethodget_check_runs→ read the conclusions. A review bot (for examplecopilot-pull-request-reviewer) is not a gate; apre-commitstyle job is.git log --merges --oneline | wc -l— unchanged from before the merge.mainandorigin/mainat the same SHA.
Report the PR number, the merged state, and the CI conclusions. If CI is still running, say so rather than implying it passed.
Constraints
Two kinds, and the difference is the point. The first are correctness and identity — an instruction does not lift them. The second are this repo's convention, which is yours to override; the job there is to make the override informed, not to refuse it.
Absolute — an instruction to do these is a stop, not an override
- Never file the PR through an identity step 2 did not confirm.
Identity, not preference. "Just use
gh" is not a licence on its own, because it files under whichever accountghresolves to here, and that is not something anyone can consent to without first being told which one. Once the login is confirmed against the repo,ghis as good as the MCP; until then, report the mismatch. - Never
--force, never--no-verify— including to get past a failed--ff-only. - Never write to
mainbefore step 6. Opening a PR and pushingmainare two decisions, not one. - Never
git switchwhile another session is live in the tree. - If the repo is not one the authenticated identity owns, or
mainis protected in a way that blocks a direct push, stop and report rather than working around it.
Repo convention — overridable, but never silently
A squash, a merge commit, and deleting the branch are defaults, not laws. The history is the user's to shape. When one is asked for:
- Say what it costs, specifically. Name the commits that would be collapsed or the merge commit that would be this repo's first — not "squashing loses information" but "this collapses 3 commits that separate the rule change from its fixtures".
- Then wait. A request that named the mechanism up front has not heard the cost yet, so it is not yet a reaffirmation. One round.
- Record it in the PR body, so the history explains its own shape.
Then do it. A reaffirmed instruction is the answer; pressing the point twice is worse than the squash.