{"id":"hoangsonww-push-to-forked-pr","name":"push-to-forked-pr","summary":"現在の作業ツリーを、新しいブランチを作成せず、通常は上流の「オリジン」にプッシュせずに、**fork**の頭を持つGitHub PRに直接プッシュします。","body":"# push-to-forked-pr\n\nThe whole point of this skill: **`origin` is usually the upstream, but the PR's head is on someone else's fork.** Pushing to `origin` updates the upstream's branch, not the PR. You have to push to the fork.\n\nGitHub allows this in two situations:\n\n1. You are the **fork owner** — straightforward, you own that branch.\n2. You are an **upstream maintainer** and the PR has `maintainerCanModify: true` (the \"Allow edits from maintainers\" checkbox the PR author leaves on by default). GitHub then lets the upstream's auth push to the fork branch.\n\nIf neither holds, abort and tell the user only the fork owner can push.\n\n---\n\n## Procedure\n\n### 1. Read the PR's head metadata\n\n```bash\nPR_NUMBER=<N>\ngh pr view \"$PR_NUMBER\" --json state,headRefName,headRefOid,headRepository,headRepositoryOwner,maintainerCanModify,url\n```\n\nCapture:\n\n- `state` — bail if not `\"OPEN\"`.\n- `headRepository.nameWithOwner` → the **fork** (e.g. `someone/Repo`).\n- `headRepositoryOwner.login` → the fork owner.\n- `headRefName` → the branch on the fork (usually matches local).\n- `headRefOid` → the PR's current head SHA. Your local HEAD must be a descendant.\n- `maintainerCanModify` → must be `true` if you are not the fork owner.\n\nIf `headRepository.nameWithOwner` matches the upstream's `nameWithOwner`, this PR is **internal**:\n\n```bash\nHEAD_BRANCH=$(gh pr view \"$PR_NUMBER\" --json headRefName --jq .headRefName)\nHEAD_REF_OID=$(gh pr view \"$PR_NUMBER\" --json headRefOid --jq .headRefOid)\ngit merge-base --is-ancestor \"$HEAD_REF_OID\" HEAD && echo ok || echo NOT-DESCENDANT   # abort if NOT-DESCENDANT\ngit push origin \"HEAD:${HEAD_BRANCH}\"\n```\n\nExit the skill once this push lands — do not continue to the fork-push steps below.\n\n### 2. Confirm you can actually push\n\n```bash\ngh auth status                        # who am I?\ngit remote get-url origin             # fetch URL — confirm origin is the UPSTREAM, not the fork\ngit remote get-url --push origin      # push URL — can differ from the fetch URL if `pushurl` is configured\n```\n\nFor the internal-PR path above, compare the **push** URL (not just the fetch URL) against the expected upstream `nameWithOwner` before relying on `git push origin` — a configured `pushurl` can silently redirect the push to a different destination than the fetch URL suggests.\n\nYou may push to the fork branch iff the active gh user is:\n\n- the fork owner, **or**\n- a writer on the upstream **and** `maintainerCanModify == true`.\n\nOtherwise stop. Tell the user the PR doesn't allow maintainer edits.\n\n### 3. Sanity-check the local branch\n\n```bash\ngit branch --show-current\ngit rev-parse HEAD\ngit merge-base --is-ancestor <headRefOid> HEAD && echo ok || echo NOT-DESCENDANT\n```\n\n- If `headRefName` differs from the current local branch, that's fine — you'll push with an explicit refspec `local:headRefName` in step 6.\n- If local HEAD is **not** a descendant of `headRefOid`, stop. Either you're on the wrong branch, or someone else has pushed to the PR since you forked from it. Do not force-push without explicit user permission.\n\n### 4. Verify commit author identity before committing\n\n```bash\ngit config user.email\n```\n\nIf it is empty, hostname-shaped (`user@host.local`, `*@*.tail*.ts.net`, etc.), or otherwise not a real email tied to a GitHub account, the commit will appear \"unverified\" on GitHub and won't link to a profile. Override per-commit:\n\n```bash\ngit -c user.name=\"<Name>\"  -c user.email=\"<email>\"  commit -F /tmp/commit-msg.txt\n```\n\nTo correct an already-made commit before push:\n\n```bash\ngit -c user.name=\"...\" -c user.email=\"...\" commit --amend --reset-author -C HEAD --no-edit\n```\n\nPrefer the user's documented identity (e.g. from `CLAUDE.md` or earlier in the session) over the local git config when the local config is clearly machine-generated.\n\n### 5. Run the repo's full verification suite — **before** pushing\n\nThe user almost always asks for this explicitly. In this repo (see `CLAUDE.md` for the authoritative list):\n\n```bash\nnpm run format:check                        # tracked files only; local .worktrees/ noise is not yours to fix\nnpm run test:server                         # backend touched\nnpm run test:client                         # frontend, wiki i18n, screen snapshots\nnpm run mcp:typecheck && npm run mcp:build  # mcp/ touched\nnpm --prefix mcp test                       # mcp unit suite\nnpm run build                               # production client build\nbash .claude/skills/file-headers/scripts/check-headers.sh\n```\n\nPer-package extras when those areas are touched:\n\n```bash\nnpm --prefix desktop run build && npm --prefix desktop test\nnode scripts/validate-agent-extensions.js\n```\n\nStop on the first red. Report which check failed and **do not push**.\n\n### 6. Stage, commit, push to the fork (not origin)\n\nExclude session-local noise (`.claude/settings.local.json` is harness state, not work) and never stage blindly:\n\n```bash\ngit status                          # review exactly what changed\ngit add <path1> <path2> ...         # stage only the files for this PR's change — never `git add -A`\ngit status                          # confirm the staged diff matches intent (and excludes .claude/settings.local.json) before committing\n```\n\nCommit with a real body (use `-F` for multi-paragraph messages), ending with the `Co-Authored-By` trailer your harness requires — copy it verbatim from the harness instructions rather than from this file, so the model name never goes stale.\n\nThen push to the fork. Add a one-off remote so it's clear in `git remote -v` and the destination URL doesn't end up in the user's shell history:\n\n```bash\nFORK_REPO=$(gh pr view \"$PR_NUMBER\" --json headRepository --jq '.headRepository.nameWithOwner')\nHEAD_BRANCH=$(gh pr view \"$PR_NUMBER\" --json headRefName   --jq .headRefName)\nLOCAL_BRANCH=$(git branch --show-current)\n\nif [ -z \"$LOCAL_BRANCH\" ]; then\n  echo \"Detached HEAD — refusing to push (source ref would be empty and delete ${HEAD_BRANCH} on the fork).\" >&2\n  exit 1\nfi\n\ngit remote add \"pr${PR_NUMBER}-fork\" \"https://github.com/${FORK_REPO}.git\"\ngit push \"pr${PR_NUMBER}-fork\" \"HEAD:${HEAD_BRANCH}\"    # push HEAD itself, not \"$LOCAL_BRANCH\" — safe even if the local branch name differs\n```\n\n`gh` configures git's credential helper, so HTTPS pushes pick up the right token automatically. Don't rewrite to SSH unless asked.\n\n### 7. Verify the push landed on the PR\n\n```bash\nLOCAL_HEAD=$(git rev-parse HEAD)\nPR_HEAD=$(gh pr view \"$PR_NUMBER\" --json headRefOid --jq .headRefOid)\n[ \"$LOCAL_HEAD\" = \"$PR_HEAD\" ] && echo \"PR updated ✓\" || echo \"MISMATCH — push went somewhere else\"\n```\n\nReport the new HEAD SHA, the PR URL, and the diff stat back to the user.\n\n---\n\n## Common failure modes\n\n| Symptom | Cause | Fix |\n|---|---|---|\n| `Permission denied` on push | Authenticated as the wrong user, or `maintainerCanModify: false` and you are not the fork owner | Abort. Only the fork owner can push to the branch. |\n| `! [rejected] non-fast-forward` | Someone else pushed to the PR since you started | `git fetch \"pr${PR_NUMBER}-fork\" \"${HEAD_BRANCH}\"`, then rebase or merge. **Never** force-push someone else's PR branch without explicit permission. |\n| Push succeeds, but PR head SHA doesn't update | You pushed to a branch with a different name on the fork | Re-check `headRefName` and use explicit refspec `LOCAL:HEAD_BRANCH`. |\n| Commit shows \"unverified\" on the PR | Author email isn't tied to a verified GitHub account | Amend with the user's real identity (step 4) and push again (fast-forward, not force). |\n| `Could not resolve host` | Network or proxy issue | Surface the error. Don't blanket-retry. |\n\n---\n\n## Hard rules\n\n- **Never push to `origin`** when origin is the upstream and the PR head is a fork. Always verify with `git remote -v` and `gh pr view`.\n- **Never create a new branch.** The user pushed because they want *this* PR updated; a new branch defeats that.\n- **Never force-push** to a fork PR branch without explicit user permission. The fork owner will lose any commits they had locally.\n- **Always run the repo's tests/builds first.** A fork-PR push triggers CI on the fork *and* surfaces on the upstream's PR view — pushing red code is doubly visible.\n- **Always exclude `.claude/settings.local.json`** unless the user explicitly says to include it. It's harness session state, not the work.\n- **Never stage with `git add -A`.** Stage explicit paths for the requested change so unrelated files or local secrets can't ride along.","author":"@hoangsonww","ownerProfile":null,"authorContacts":null,"sourceUrl":"https://github.com/hoangsonww/Claude-Code-Agent-Monitor/tree/master/.claude/skills/push-to-forked-pr","license":"MIT","category":null,"lang":"en","tokens":2117,"stars":0,"calls30d":1,"claimed":false,"visibility":"public","origin":"crawler","version":"0.1.0","createdAt":"2026-08-22","updatedAt":"2026-08-22","files":[],"requires":{"mcp":[],"tools":[]},"safety":{"flags":[],"scannedAt":"2026-08-22","hasScripts":false,"networkEndpoints":[]}}