Skip to content
Back to skills

Kc Ship Pr

DSecurity

Commit 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.

  • 399 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added September 9, 2026
toolspythongoshellbashtestinggitapi

Works with

  • claude code
  • api

Security analysis

D50/100
  • criticalExfiltrates credentials via HTTP — exact pattern from Snyk ToxicSkills study
  • criticalSends environment variables or credentials to an external URL

Pro shows the line behind each finding and how to fix it

Scanned November 9, 2026

npx -y skills add imran31415/kube-coder --skill kc-ship-pr --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Kc Ship Pr?

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

Security grade badge for Kc Ship Pr
[![Security: D — Skills Directory](https://www.skillsdirectory.com/api/skills/imran31415-kc-ship-pr/badge)](https://www.skillsdirectory.com/skills/imran31415-kc-ship-pr)

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: kc-ship-pr
description: Commit 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.
user-invocable: true
allowed-tools: Bash, Read
argument-hint: "[PR title] (optional; inferred from the commit if omitted)"
---

# Ship a PR from a kube-coder workspace

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 push` is wedged** — `remote: Invalid username or token` or
> `could 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/user` proves it in one line). Diagnose in this order:
>
> ```bash
> 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-credentials
> ```
>
> Three known causes, all "a token frozen an hour ago":
> 1. A **stale baked `http.<host>.extraheader`** in `.git/config` — git sends it
>    verbatim and it shadows the helper: `git config --unset-all
>    http.https://github.com/.extraheader`.
> 2. A **host-scoped helper chain reset** (issue #454) — `gh auth setup-git`
>    writes an empty `helper =` under `[credential "https://github.com"]`, which
>    *clears* the chain and drops the self-refreshing reader; `gh auth
>    git-credential` then answers with the **stale `GH_TOKEN`** your 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, run
>    `python3 /github-app/github-app-token.py --configure-git` (or `--once`,
>    which also asks the `github-app-token` sidecar for a fresh token; the
>    private key lives only in that sidecar since #558, so nothing in your
>    shell can mint one).
> 3. A **stale `github.com` line in `~/.git-credentials`** (persistent on the
>    PVC, so a captured `ghs_…` 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:
> ```bash
> 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-auth` memory 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

```bash
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)

```bash
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

```bash
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`:

```bash
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, then
  `PATCH /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 `FILES` change; everything else is inherited from `base_tree`.

### 4. Open the PR

```bash
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:

```bash
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:

1. **kc-scope-pr** — what the diff actually reaches, which tests cover it, what
   it made worse, what it left untested. Its `impacted=` and `untested=` 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.
2. **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-auth` memory 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 push` again; if you're using
  the §3b fallback, repeat it in PATCH mode onto the branch head.


### Publishing a retained completed Build (#710)

After the agent session has ended, the human can use Changes ? Prepare review ? Push & Open PR on web or phone. This reviews a snapshot and runs a durable server-owned commit/push/PR operation. It discovers existing head/base PRs and uses the configured repository, base and GitHub account. Do not invoke this post-session API from an active agent that holds the worktree: its live-writer gate intentionally refuses that. Existing explicit in-agent `KC_AUTO_PR=1` and shipping instructions keep their current path; the UI discovers their PR afterward.

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.

View all in tools →

Comments

Loading comments…

Newsletter

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