Standard: Worker GitHub CLI Authentication
Requirement
Every AI-Dev worker’s gh CLI must be authenticated as its own AI-Dev GitHub account —
not as radieu (owner) and not via an inherited GITHUB_TOKEN env var.
This is a required compliance standard. The infra_docs_check audit action verifies it weekly.
Worker registry (2026-07-11)
| Worker | Host | Linux user | Required gh login | git identity |
|---|---|---|---|---|
AI-Dev-IO1 | vps-i1 217.154.82.162 | claude-runner | AI-Dev-IO1 | ai-dev-io1@zintegrowana.online |
AI-Dev-BMS4-1 | bms-4 54.36.123.110 | claude-runner | AI-Dev-BMS4-1 | ai-dev-bms4-1@zintegrowana.online |
AI-Dev-BMS4-2 | bms-4 54.36.123.110 | claude-runner-2 | ai-dev-bms4-2 | ai-dev-bms4-2@zintegrowana.online |
AI-Dev-WinDev-1 | windows-dev (local workstation) | n/a (multi-account gh, not a Linux user) | ai-dev-windev-1 | not set globally — see note below |
Email routing: all ai-dev-*@zintegrowana.online forward to radieu@gmail.com via Cloudflare.
AI-Dev-WinDev-1 — different pattern (2026-07-11, issue: shared rate-limit exhaustion)
Unlike the SSH workers above, this is not a standalone claude-agent runner with its own host/cron —
it exists solely so local, gh-heavy Claude Code skills (/issues-review, /triage-issues,
nightly-devops-triage, etc.) run on this Windows dev machine don’t burn radieu’s personal
5,000/hr core REST rate limit (GitHub’s rate limit is per-authenticated-user, not per-token — a second
PAT on the radieu account would NOT create a separate bucket, only a genuinely separate account does).
- Registered via
gh auth logindevice flow (OAuth token, no expiry, stored in Windows keyring) - Both
radieuandai-dev-windev-1are logged in simultaneously (gh auth statusshows both);radieuis left as the default active account for normal interactive use - Switch to it only for automation-heavy read/write gh calls:
gh auth switch -u ai-dev-windev-1, then switch back:gh auth switch -u radieu - git identity intentionally NOT changed globally — this machine’s global
git configstaysradieu@gmail.com/Radek Konarskiso the human’s own commits aren’t misattributed. Only set a local per-repo/per-command git identity toai-dev-windev-1if a skill run under that account needs to author a commit. - Registered in
dev_r_servicesasservice_type='secret',element_type='credential'(notautomation/claude-agentlike the SSH workers — it has no running process of its own)
Repo collaborator access (added 2026-07-11)
Being authenticated is not enough — ai-dev-windev-1 also needs to be a collaborator on each
repo it pushes to / triggers workflows on, same as the SSH worker accounts (AI-Dev-IO1,
AI-Dev-BMS4-1, ai-dev-bms4-2). Discovered when gh workflow run wasabi-sops-update.yml failed
with “Could not resolve to a Repository” under the switched account.
Granted write (push) role — never admin, matching every other bot account:
| Repo | Owner |
|---|---|
p24-infra | radieu (personal) |
et-operational-platform | radieu (personal) |
radekkonarski-personal-brand | radieu (personal) |
brandpilot | p24-infra GitHub org — not radieu personal (see note below) |
Note — p24-infra is also a GitHub org, distinct from the radieu/p24-infra repo. brandpilot
lives at p24-infra/brandpilot (org), not under radieu. Easy to miss — gh repo list radieu
won’t show it. Check gh api user/orgs and gh repo list p24-infra when a repo referenced in
CLAUDE.md secrets docs (e.g. brandpilot.env.sops) isn’t found under radieu.
To add a new bot account as collaborator (repo invitations require accepting from the invitee’s
own gh session):
# 1. As radieu (owner/admin), invite:
gh api -X PUT "repos/<owner>/<repo>/collaborators/<bot-account>" -f permission=push
# 2. Switch to the bot account and accept:
gh auth switch -u <bot-account> --hostname github.com
$invites = gh api user/repository_invitations | ConvertFrom-Json
$invites | ForEach-Object { gh api -X PATCH "user/repository_invitations/$($_.id)" }
gh auth switch -u radieu --hostname github.comgh api rate_limit --jq '.rate' shares one 5,000/hr bucket per authenticated user — a long session
of heavy gh api/gh pr/gh workflow calls under radieu can exhaust it (remaining: 0); this is
exactly the failure mode ai-dev-windev-1 exists to route around. If radieu shows exhausted,
wait for .rate.reset (unix timestamp) rather than retrying in a loop.
Verify (run anytime)
# From local workstation — check all three workers
foreach ($w in @(
"54.36.123.110|claude-runner|AI-Dev-BMS4-1",
"54.36.123.110|claude-runner-2|AI-Dev-BMS4-2",
"217.154.82.162|claude-runner|AI-Dev-IO1"
)) {
$ip, $user, $label = $w -split "\|"
$login = ssh "root@$ip" "su -s /bin/bash $user -c 'gh api /user --jq .login 2>&1'"
$email = ssh "root@$ip" "su -s /bin/bash $user -c 'git config --global user.email 2>/dev/null'"
$ok = if ($login -eq ($label.ToLower() -replace "ai-dev-","ai-dev-")) { "OK" } else { "FAIL" }
Write-Host "$label | gh=$login | email=$email | $ok"
}Expected output:
AI-Dev-BMS4-1 | gh=AI-Dev-BMS4-1 | email=ai-dev-bms4-1@zintegrowana.online | OK
AI-Dev-BMS4-2 | gh=ai-dev-bms4-2 | email=ai-dev-bms4-2@zintegrowana.online | OK
AI-Dev-IO1 | gh=AI-Dev-IO1 | email=ai-dev-io1@zintegrowana.online | OK
Fix: re-authenticate a worker (device flow)
Run when a worker shows wrong gh login or radieu instead of its own account.
# 1. Install tmux if missing (AlmaLinux: dnf, Ubuntu: apt-get)
ssh root@<IP> "dnf install -y tmux 2>/dev/null || apt-get install -y tmux -q"
# 2. Start device flow in tmux, capture the code
ssh root@<IP> "tmux kill-session -t ghauth 2>/dev/null; rm -f /tmp/ghauth.log; tmux new-session -d -s ghauth \"su -s /bin/bash <linux-user> -c 'unset GITHUB_TOKEN; gh auth login --hostname github.com --git-protocol https --web 2>&1 | tee /tmp/ghauth.log; echo AUTH_DONE >> /tmp/ghauth.log'\""
Start-Sleep 8
ssh root@<IP> "cat /tmp/ghauth.log"- Open
https://github.com/login/devicelogged in as the AI-Dev account (not as radieu). - Enter the code shown in output. Click Authorize.
# 5. Wait for completion and switch active account
Start-Sleep 5
ssh root@<IP> "cat /tmp/ghauth.log" # should show "Logged in as AI-Dev-XXX"
ssh root@<IP> "su -s /bin/bash <linux-user> -c 'gh auth switch -u <AI-Dev-XXX> 2>&1'"vps-i1 extra step — unset inherited GITHUB_TOKEN
On vps-i1, root’s SSH session has an inherited GITHUB_TOKEN (source unknown, likely GH Actions
runner process namespace). Without the fix below, su inherits it and gh uses radieu instead.
The fix is already applied: /home/claude-runner/.bashrc ends with unset GITHUB_TOKEN.
If re-provisioning vps-i1 from scratch, re-apply:
printf '\n# Override inherited GITHUB_TOKEN from root/runner env\nunset GITHUB_TOKEN\n' \
>> /home/claude-runner/.bashrcFix: set git identity
ssh root@<IP> "su -s /bin/bash <linux-user> -c '
git config --global user.email \"ai-dev-xxx@zintegrowana.online\"
git config --global user.name \"AI-Dev-XXX\"
'"Adding a new worker
When adding any new AI-Dev account to the ecosystem:
- Create
ai-dev-xxx@zintegrowana.onlinein Cloudflare Email Routing (forward toradieu@gmail.com) - Create GitHub account using that email
- Run device flow (Step 2 above) to authenticate
ghas that account - Set git identity (Step 3 above)
- Verify with the check script above
- Register in
dev_r_services(seedocs/playbooks/new-repo-setup.md §Step 7) — populate the full rotation schedule at insert time per §Rotation policy (criticality,rotation_freq,last_rotated,next_due); a row left with a NULLnext_duefiresCredentialRotationOverdueimmediately. - Add to this registry table
Token scope requirements
| Scope | Required for |
|---|---|
repo | Accepting repo invitations, pushing code, creating PRs |
read:org | Checking org membership |
workflow | Triggering GH Actions workflows |
gist | (Optional — not required, but present on bms-4 tokens) |
OAuth tokens obtained via device flow automatically get these scopes when authorized by a user who has them on their account.
Rotation policy
Device-flow gh OAuth tokens do not have a hard expiry (they can only be revoked), so they
are not driven by an expiry deadline. They still carry repo + workflow write scopes across
every repo the account collaborates on, so they are governed under a periodic hygiene rotation
cadence rather than being exempted from rotation tracking.
Field (dev_r_services) | Value | Rationale |
|---|---|---|
criticality | high | Write scopes across 4 repos (push code, trigger workflows), but a secondary automation identity — not the primary GH_PAT/GH_TOKEN (those are critical). |
compliance_password_rotation | no | Manual rotation required — kept under governance, not n/a. |
rotation_freq | 180d | Matches other high-criticality, non-auto-rotated tokens (e.g. SUPABASE_SERVICE_KEY, WAHA_*). |
auto_rotate | false | Rotation is interactive (revoke + re-run device flow on the workstation) — cannot be automated. |
Set last_rotated to the device-flow (re-)auth date and next_due to last_rotated + 180d.
AI-Dev-WinDev-1 was registered 2026-07-11 → next_due = 2027-01-07.
Why this matters: the
credential-exportertreats acompliance_password_rotation='no'credential with a NULLnext_dueas maximally overdue (days=999,app.py:84-91). A new credential row must therefore be created with its full rotation schedule populated, or it will fireCredentialRotationOverduethe moment it goesactive. Follow this policy when running §Adding a new worker step 6.
Rotate (manual): revoke the token on GitHub (Settings → Applications, or gh auth logout +
re-gh auth login device flow under the AI-Dev account), then update last_rotated/next_due
in dev_r_services for that credential row.
Escalation
Token expires: gh OAuth tokens (device flow) don’t expire but can be revoked. If gh api /user
returns 401, re-run the device flow.
radieu shows as active despite fix: Ensure unset GITHUB_TOKEN is in .bashrc (vps-i1 only).
On bms-4 this is not needed — GITHUB_TOKEN is not inherited.
Attribution wrong in commits/PRs: Verify git config --global user.email and user.name
match the expected AI-Dev identity for that runner.