Skip to content

feat(credentials): select GH_TOKEN by GitHub owner of launch directory - #123

Merged
twistedmelonman merged 1 commit into
mainfrom
claude/owner-keyed-gh-token-2131
Sep 16, 2026
Merged

twistedmelonman merged 1 commit into
mainfrom
claude/owner-keyed-gh-token-2131

Conversation

@twistedmelonman

Copy link
Copy Markdown
Member

Problem

A fine-grained PAT is bound to exactly one GitHub resource owner at creation
and cannot be repointed. The 2026-09 org migration moved repos out from under
the single personal-owner token, so gh lost access to every org repo — while
SSH-based git kept working, which is why it went unnoticed for ~10 days.

This is step 2 of the plan in #120. Step 1 (fail closed on vault failure)
shipped in #122.

Fix

Derive the GitHub owner from the launch directory's origin remote, and select
the matching vault item:

Owner Vault item
smartwatermelon op://Automation/CCCLI-SWM/token
nightowlstudiollc op://Automation/CCCLI-NOS/token
anything else, or not derivable op://Automation/GitHub - CCCLI/Token

One op read per launch — the token is selected, not accumulated. Unknown or
underivable owners fall back to the personal token, preserving the previous
behavior for sessions launched outside a repo.

Deviation from the sketch in #120

The issue proposed a sed substitution to extract the owner. A non-matching
sed passes its input through unchanged, so a non-GitHub remote returned the
entire URL as the owner:

gitlab remote -> [https://gitlab.com/someone/thing.git]

That still lands in the *) fallback, so the token choice was right by
accident — but a URL would have been printed where an owner name belongs.
Replaced with explicit prefix matching plus a charset check. This also fixes
ssh://git@github.com/owner/repo, a valid remote form the scp-style pattern
does not match.

Token verification (live, before this change)

Both new PATs were checked against real repos:

Check CCCLI-SWM CCCLI-NOS
Private org repos ok 10 (was 0)
Administration: read ok ok
Actions: read 428 runs 353 runs
Pull requests: read ok ok
Issues: write posted + deleted posted + deleted

Administration: read is the permission whose absence produced the HTTP 403
that made the pre-merge hook treat every check as blocking during #122.
Issues: write is what blocked posting a comment to #120 from this session.
Both scratch comments were deleted and verified 404.

End-to-end verification

Through bin/claude-wrapper itself — not a sourced library — with GH_TOKEN
unset:

claude-wrapper  -> smartwatermelon   -> op://Automation/CCCLI-SWM/token
amelia-boone    -> nightowlstudiollc -> op://Automation/CCCLI-NOS/token
~               -> <none>            -> op://Automation/GitHub - CCCLI/Token

And the selected tokens do real work: from claude-wrapper, branch protection
reads ["claude-review / run-review"]; from amelia-boone, 10 private NOS
repos are visible.

Tests

10 new cases: both org owners, the personal owner, https/scp/ssh:// remote
forms, unknown owners, non-GitHub remotes, a repo with no origin, a non-repo
directory, and a subdirectory of a repo. Every one was confirmed to fail
against broken code before being accepted
— they failed twice during
development, for two different real reasons.

  • test-credentials.sh 24/24
  • test-launch-dir-check.sh 6/6
  • test-remote-session.sh 26/26
  • test-wrapper.sh 64/64
  • shellcheck -S info clean

test-gh-token-permissions.sh was deliberately not run: it mutates a live
sandbox repo.

Also in this PR

Shared test-helper bugfix. make_stub_dir used command -v to locate
binaries, which returns the name rather than a path when the caller's shell
has a function of that name. git is a function in this environment, so the
helper created a broken symlink and git appeared absent inside every stub
that requested it. Switched to type -P. This changes behavior for every test
using the helper, not only the new ones.

Docs. CLAUDE.md and README.md both documented a single hardcoded ref,
and README explicitly claimed the wrapper "does not perform per-org token
routing" — which this makes false. Also corrects two stale claims in CLAUDE.md
that #122 had already disproved: that these credentials are absent from child
process environments, and that a failed fetch falls back to the keyring.

Follow-up

The GitHub App migration (#120's "alternative considered") is deliberately out
of scope — the issue describes it as its own project. Filed separately so it is
not lost with #120 closed.

Closes #120

https://claude.ai/code/session_011r87TBgqE338FGJn5zpZ52

A fine-grained PAT is bound to exactly one resource owner at creation and
cannot be repointed afterward. The 2026-09 org migration moved repos out from
under the single personal-owner token, so gh lost access to every org repo
while SSH-based git kept working — which is why the regression survived ~10
days unnoticed.

Derive the owner from the launch directory's origin remote and select the
matching vault item:

  smartwatermelon    -> op://Automation/CCCLI-SWM/token
  nightowlstudiollc  -> op://Automation/CCCLI-NOS/token
  otherwise          -> op://Automation/GitHub - CCCLI/Token

One op read per launch; the token is selected, not accumulated. Unknown or
underivable owners fall back to the personal token, preserving pre-migration
behavior for sessions launched outside a repo.

Owner parsing uses prefix matching rather than the sed substitution sketched
in #120. A non-matching sed expression passes its input through unchanged, so
a non-GitHub remote yielded the entire URL as the "owner" — harmless at the
case statement but wrong in the debug log. Explicit prefixes also handle
ssh://git@github.com/owner/repo, which the scp-style pattern alone misses.

Verified end-to-end through bin/claude-wrapper itself, not just a sourced
library, with GH_TOKEN unset:

  claude-wrapper  -> smartwatermelon   -> CCCLI-SWM
  amelia-boone    -> nightowlstudiollc -> CCCLI-NOS
  ~               -> <none>            -> personal

Both new tokens were verified against live GitHub before this change: org repo
access (NOS private repos 0 -> 10), Administration:read (the branch-protection
403 that blocked the #122 merge), Actions:read, Pull requests:read, and
Issues:write (scratch comment posted and deleted on each owner).

Adds 10 tests covering both org owners, the personal owner, https/scp/ssh://
remote forms, unknown owners, non-GitHub remotes, a repo without an origin, a
non-repo directory, and a subdirectory of a repo. All ten were confirmed to
fail against broken code before being accepted.

Also fixes make_stub_dir, a shared test helper: it used `command -v` to locate
binaries, which returns the NAME rather than a path when the caller's shell has
a function of that name. `git` is a function in this environment, so the helper
created a broken symlink and git appeared absent inside every stub that
requested it. Switched to `type -P`, which searches PATH only. This changes
behavior for every test using that helper, not only the new ones.

Docs: CLAUDE.md and README.md described a single hardcoded token ref, and
README explicitly claimed the wrapper "does not perform per-org token routing",
which this change makes false. Both now document the selection table. Also
corrects two stale claims in CLAUDE.md's credentials.sh entry that #122 had
already disproved: that the credentials are absent from child environments,
and that a failed fetch falls back to the keyring.

test-gh-token-permissions.sh was deliberately not run: it mutates a live
sandbox repo.

  test-credentials 24/24, test-launch-dir-check 6/6,
  test-remote-session 26/26, test-wrapper 64/64, shellcheck -S info clean

Closes #120

Claude-Session: https://claude.ai/code/session_011r87TBgqE338FGJn5zpZ52
@claude

This comment has been minimized.

@twistedmelonman
twistedmelonman merged commit d884fe6 into main Sep 16, 2026
4 checks passed
@twistedmelonman
twistedmelonman deleted the claude/owner-keyed-gh-token-2131 branch September 16, 2026 01:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GH_TOKEN lost org repo access after 2026-09 org migration; fine-grained PAT is single-owner

1 participant