Ship a PR from a kube-coder workspace
SkillDev toolsCommit local changes and open a pull request against kube-coder from inside a workspace pod. Use when the user wants to push a branch or open/update a PR with the workspace GitHub App token.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Ship a PR from a kube-coder workspace skill
What this skill tells your AI
The instructions your AI receives, as published by imran31415/kube-coder in .claude/skills/kc-ship-pr/SKILL.md and read by ahel’s review.
The only GitHub credential here is the App installation token
(/home/dev/.credentials/.github-token, ghs_…). The token-refresh sidecar
installs a self-refreshing global credential.helper that reads that file
fresh on every call, so ordinary git push works — no Git Data API dance
needed. gh api also works (it reads GITHUB_TOKEN from ~/.github-env).
There is no user-level gh auth login and no SSH key here, so do not suggest
gh auth login or a fork — the App token pushes to origin directly.
If
git pushis wedged —remote: Invalid username or tokenorcould not read Username— something is shadowing that helper with a point-in-time token. The token file itself is almost never the problem (curl -H "Authorization: token $(cat /home/dev/.credentials/.github-token)" https://api.github.com/userproves it in one line). Diagnose in this order:git config --show-origin --get-all credential.helper git config --show-origin --get-all credential.https://github.com.helper printf 'protocol=https\nhost=github.com\n\n' | git credential fill # vs. the token file grep github.com ~/.git-credentialsThree known causes, all "a token frozen an hour ago":
- A stale baked
http.<host>.extraheaderin.git/config— git sends it verbatim and it shadows the helper:git config --unset-all http.https://github.com/.extraheader.- A host-scoped helper chain reset (issue #454) —
gh auth setup-gitwrites an emptyhelper =under[credential "https://github.com"], which clears the chain and drops the self-refreshing reader;gh auth git-credentialthen answers with the staleGH_TOKENyour long-lived shell captured before the last rotation. The workspace re-asserts that section every 50 min, so this self-heals — to fix it now, runpython3 /github-app/github-app-token.py --configure-git(or--once, which also asks thegithub-app-tokensidecar for a fresh token; the private key lives only in that sidecar since #558, so nothing in your shell can mint one).- A stale
github.comline in~/.git-credentials(persistent on the PVC, so a capturedghs_…outlives its 1-hour validity). The daemon purges these in app mode; delete by hand if you're ahead of it.One-shot escape hatch that bypasses all three:
git -c credential.helper= \ -c 'credential.helper=!f() { echo username=x-access-token; echo "password=$(cat /home/dev/.credentials/.github-token)"; }; f' \ push -u origin <branch>Only if push is still broken after that, fall back to the Git Data API (§3b). See your
github-authmemory for the full background.
Steps
Given the working tree already has the changes staged/committed on a feature
branch (create one first if the user is on main):
1. Commit locally
cd /home/dev/kube-coder # or your worktree
git add -A # or specific paths
git commit -m "<type>(<scope>): <summary>
<body>
Co-Authored-By: Claude <noreply@anthropic.com>"
2. Re-sync onto current origin/main (avoid a stale base)
git fetch origin main && git rebase origin/main
If the credential helper is somehow unavailable, the repo is public so an
unauthenticated fetch also works:
git -c credential.helper= -c http.https://github.com/.extraheader= fetch https://github.com/imran31415/kube-coder.git main
3. Push the branch
git push -u origin <branch>
3b. Fallback — push via the Git Data API (only if git push stays wedged)
Run this Python (token from env or the credential file). Set BRANCH and list
the changed files in FILES:
source /home/dev/.credentials/.github-env # exports GITHUB_TOKEN
python3 - <<'PY'
import os, json, base64, urllib.request, subprocess
TOKEN = os.environ["GITHUB_TOKEN"]; REPO = "imran31415/kube-coder"
API = f"https://api.github.com/repos/{REPO}"
BRANCH = "REPLACE-branch-name"
FILES = subprocess.check_output(
["git", "diff", "--name-only", "main", "HEAD"]).decode().split()
def api(method, url, body=None):
d = json.dumps(body).encode() if body is not None else None
r = urllib.request.Request(url, data=d, method=method)
r.add_header("Authorization", f"token {TOKEN}")
r.add_header("Accept", "application/vnd.github+json")
if d: r.add_header("Content-Type", "application/json")
with urllib.request.urlopen(r) as x: return json.load(x)
base = api("GET", f"{API}/git/refs/heads/main")["object"]["sha"]
tree = api("GET", f"{API}/git/commits/{base}")["tree"]["sha"]
entries = []
for f in FILES:
blob = api("POST", f"{API}/git/blobs",
{"content": base64.b64encode(open(f, "rb").read()).decode(),
"encoding": "base64"})
entries.append({"path": f, "mode": "100644", "type": "blob", "sha": blob["sha"]})
t = api("POST", f"{API}/git/trees", {"base_tree": tree, "tree": entries})
msg = subprocess.check_output(["git", "log", "-1", "--format=%B"]).decode().strip()
commit = api("POST", f"{API}/git/commits",
{"message": msg, "tree": t["sha"], "parents": [base]})
# New branch: POST a ref. To UPDATE an existing branch (add a commit), use
# PATCH {API}/git/refs/heads/{BRANCH} with {"sha": commit["sha"]} instead.
api("POST", f"{API}/git/refs",
{"ref": f"refs/heads/{BRANCH}", "sha": commit["sha"]})
print("pushed", commit["sha"][:8], "->", BRANCH)
PY
Notes:
- New branch →
POST /git/refs. Add a commit to an existing PR branch → read the branch's current head first, base the tree on it, thenPATCH /git/refs/heads/{BRANCH}with the new commit sha. - The commit author is the App (bot), not the user — expected and fine for a PR.
- Only the files in
FILESchange; everything else is inherited frombase_tree.
4. Open the PR
source /home/dev/.credentials/.github-env
BODY=$(cat <<'EOF'
## What & why
...
## Testing
...
🤖 Generated with [Claude Code](https://claude.com/claude-code)
EOF
)
gh api repos/imran31415/kube-coder/pulls \
-f title="$ARGUMENTS" -f head="REPLACE-branch-name" -f base="main" -f body="$BODY" \
--jq '.html_url'
If gh fails, the same works with curl -X POST -H "Authorization: token $GITHUB_TOKEN".
5. Link the issue (if the PR resolves one)
The PR body/title referencing (#N) cross-links but does not auto-close.
To close on merge, add Fixes #N to the PR body, or close the issue after merge:
gh api repos/imran31415/kube-coder/issues/N/comments -f body="Resolved by #<pr>."
gh api -X PATCH repos/imran31415/kube-coder/issues/N -f state=closed -f state_reason=completed
Before shipping
Two skills, in this order:
- kc-scope-pr — what the diff actually reaches, which tests cover it, what
it made worse, what it left untested. Its
impacted=anduntested=numbers belong in the PR body when they are large: a reviewer who sees "3 files changed" reads the diff differently than one who also sees the blast radius. - kc-preflight — runs the suites for real, so CI is green on the first push.
Never push a branch you haven't at least bash -n/typecheck/test-run locally —
CI round-trips are slow.
See also
- kc-scope-pr for the numbers that make a PR body reviewable, and the tests to run before this one.
- Your
github-authmemory has the full background on the App-token auth setup and the stale-extraheader footgun. - To add a commit to an already-open PR, just
git pushagain; if you're using the §3b fallback, repeat it in PATCH mode onto the branch head.
Signals
- GitHub stars
- 354
- Forks
- 32
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
kc-ship-pr- Source
- github.com/imran31415/kube-coder