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)

WorkerHostLinux userRequired gh logingit identity
AI-Dev-IO1vps-i1 217.154.82.162claude-runnerAI-Dev-IO1ai-dev-io1@zintegrowana.online
AI-Dev-BMS4-1bms-4 54.36.123.110claude-runnerAI-Dev-BMS4-1ai-dev-bms4-1@zintegrowana.online
AI-Dev-BMS4-2bms-4 54.36.123.110claude-runner-2ai-dev-bms4-2ai-dev-bms4-2@zintegrowana.online
AI-Dev-WinDev-1windows-dev (local workstation)n/a (multi-account gh, not a Linux user)ai-dev-windev-1not 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 login device flow (OAuth token, no expiry, stored in Windows keyring)
  • Both radieu and ai-dev-windev-1 are logged in simultaneously (gh auth status shows both); radieu is 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 config stays radieu@gmail.com / Radek Konarski so the human’s own commits aren’t misattributed. Only set a local per-repo/per-command git identity to ai-dev-windev-1 if a skill run under that account needs to author a commit.
  • Registered in dev_r_services as service_type='secret', element_type='credential' (not automation/claude-agent like 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:

RepoOwner
p24-infraradieu (personal)
et-operational-platformradieu (personal)
radekkonarski-personal-brandradieu (personal)
brandpilotp24-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.com

gh 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"
  1. Open https://github.com/login/device logged in as the AI-Dev account (not as radieu).
  2. 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/.bashrc

Fix: 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:

  1. Create ai-dev-xxx@zintegrowana.online in Cloudflare Email Routing (forward to radieu@gmail.com)
  2. Create GitHub account using that email
  3. Run device flow (Step 2 above) to authenticate gh as that account
  4. Set git identity (Step 3 above)
  5. Verify with the check script above
  6. Register in dev_r_services (see docs/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 NULL next_due fires CredentialRotationOverdue immediately.
  7. Add to this registry table

Token scope requirements

ScopeRequired for
repoAccepting repo invitations, pushing code, creating PRs
read:orgChecking org membership
workflowTriggering 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)ValueRationale
criticalityhighWrite 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_rotationnoManual rotation required — kept under governance, not n/a.
rotation_freq180dMatches other high-criticality, non-auto-rotated tokens (e.g. SUPABASE_SERVICE_KEY, WAHA_*).
auto_rotatefalseRotation 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-exporter treats a compliance_password_rotation='no' credential with a NULL next_due as 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 fire CredentialRotationOverdue the moment it goes active. 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.