github-repo-metadata · v1.0 · 2026-09-23 · sha256 452224ae96f51fdf

github-repo-metadata v1.0A

Immutable. This exact content is served forever at /api/v1/blob/452224ae96f51fdf.

---
name: github-repo-metadata
description: >
  Use when updating a GitHub repository's description, homepage, topics, or
  other metadata via the API — especially when the gh CLI is unavailable and no
  GITHUB_TOKEN is set, so authentication must come from the local git credential
  store. Also covers the read-after-write caching trap that makes a successful
  PATCH look like a silent no-op.
license: MIT
metadata:
  author: cubecloud-io
  version: "1.0"
---

# GitHub repo metadata without the `gh` CLI

## The trap this skill exists to prevent

You want to change a repo's description. You check for tooling:

- `Get-Command gh` → **absent**
- `$env:GITHUB_TOKEN` / `$env:GH_TOKEN` → **unset**
- unauthenticated `PATCH /repos/...` → **401**

The obvious conclusion is *"no auth, this is blocked, ask the user for a token."*
**That conclusion is wrong**, and it is the failure this skill diagnoses. A machine
that has ever pushed over HTTPS with `credential.helper=manager` holds a usable
token in the Windows credential store. `git push` works; the API works too — you
just have to ask the credential store instead of the environment.

**Do not report "blocked" until you have tried the credential store.** Check for
an existing credential before asking a human for anything.

## Decision order

| Check | If present | If absent |
|---|---|---|
| `gh auth status` | use `gh` (simplest) | continue |
| `$env:GITHUB_TOKEN` / `GH_TOKEN` | use it | continue |
| git credential store | use it — see below | **now** it is genuinely blocked |

## Read the credential (never print it, never write it)

```powershell
# Suppress interactive prompts FIRST or this can block on a GUI dialog.
$env:GIT_TERMINAL_PROMPT = "0"
$env:GCM_INTERACTIVE     = "never"

$raw = "protocol=https`nhost=github.com`n`n" | git credential fill 2>&1
$tok = ($raw | Where-Object { $_ -like "password=*" } | Select-Object -First 1) -replace "^password=", ""
if (-not $tok) { throw "no stored credential" }
```

Keep `$tok` **in memory only**. Never echo it, never redirect it to a file.

Confirm the credential is usable and learn what it may do:

```powershell
Invoke-WebRequest -Uri "https://api.github.com/user" -Headers @{ Authorization = "Bearer $tok"; "User-Agent" = "ps" }
# 200 -> authenticated. The x-oauth-scopes response header names the scopes.
```

`repo` scope covers repository metadata writes. A response with an **empty**
`x-oauth-scopes` header usually means a *fine-grained* PAT — those omit the scope
header, and write access depends on the per-repo permissions chosen at creation,
so read `permissions` from `GET /repos/{owner}/{repo}` instead of guessing.

## Write the metadata

```powershell
$body  = @{ description = $desc; homepage = $url } | ConvertTo-Json -Compress
$bytes = [Text.Encoding]::UTF8.GetBytes($body)   # encode explicitly; a non-ASCII
                                                 # char in a default body fails
Invoke-WebRequest -Uri "https://api.github.com/repos/$owner/$repo" -Method Patch `
  -Headers @{
    Authorization          = "Bearer $tok"
    "User-Agent"           = "ps"
    Accept                 = "application/vnd.github+json"
    "X-GitHub-Api-Version" = "2022-11-28"
  } `
  -ContentType "application/json; charset=utf-8" `
  -Body $bytes
```

GitHub enforces a **350-character** description limit — check length before sending.

## Verify — and do not trust the immediate read-back

```powershell
$r = Invoke-RestMethod -Uri "https://api.github.com/repos/$owner/$repo?ts=$(Get-Date -UFormat %s)" `
  -Headers @{ "User-Agent" = "ps"; "Cache-Control" = "no-cache" }
```

**`GET /repos/{owner}/{repo}` is cached.** A PATCH returning **200** can still read
back the *old* value seconds later. Do not conclude the write failed. Wait ~30–60s
or add a cache-busting query param, then re-read. Confirm `updated_at` moved.

## Dead ends already ruled out

- `gh` CLI — frequently simply not installed; `gh`-based skills fail entirely here.
- `GITHUB_TOKEN` / `GH_TOKEN` — commonly unset even when pushing works fine.
- unauthenticated PATCH — always 401; never a viable path.
- Existing skills `github-ops` and `attach-review-to-pr` **both assume `gh`**
  (`gh auth status`, `gh auth login`) and so do not help on a `gh`-less machine.

## Safety

- Read the token in memory; never print, log, or persist it.
- Report only non-identifying facts: length and token class
  (`gh?_` / `github_pat_` / 40-hex), plus the login from `GET /user`.
- After the run, sweep the working tree for token-shaped strings and confirm `0`:

```powershell
Get-ChildItem . -Recurse -File | Where-Object { $_.FullName -notmatch "\\\.git\\" } |
  Select-String -Pattern "\bgh[pousr]_[A-Za-z0-9]{36}\b"
```

- If the credential carries `repo` scope it is a **full-write admin credential**.
  That is normal for a dev box, but say so plainly when you use it.

## Passed verification (why this skill is trusted)

Applied to `JZKK720/cubecloud-skills-bundle-kit`: `PATCH` → **200**; after cache
expiry the live read-back showed the new description and homepage, and `updated_at`
had advanced. Negative control: the unauthenticated PATCH returned **401**, so the
success above is attributable to the stored credential and not to public write
access.