Skip to content

claude-routines bot-identity split: run overnight routines as a non-admin GitHub identity #299

@schmug

Description

@schmug

Task

Make the nightly Claude Code claude-routines open PRs as a non-admin GitHub identity so the main-protection CODEOWNERS gate (landed via #295) is actually enforced instead of bypassed. Today routines authenticate as schmug (repo admin); GitHub branch-ruleset bypass_actors keys off the admin role with bypass_mode: always, so every routine PR bypasses every protection. On a public repo whose single-issue routine prefers issues that read like Claude Code prompts (i.e. favors well-crafted external/attacker input), this is an open path from a stranger's issue to an auto-merged production deploy. Authoritative finding (Claude Code docs, verified 2026-05-16): routines always author PRs as the connected account; there is no per-routine identity config and no bot-author option — the only supported split is to repoint the claude.ai account's GitHub connection to a separate account.

Design (decisions locked)

Identity model

  • Create GitHub machine account dmarcheck-bot (2FA enabled; recovery owned by @schmug).
  • Add dmarcheck-bot to schmug/dmarcheck as a write collaborator — NOT admin, NOT added to ruleset bypass_actors.
  • Repoint the claude.ai account's GitHub connection from schmug to dmarcheck-bot (account-wide; affects all cloud sessions).
  • Net: cloud routines + cloud interactive sessions act as dmarcheck-bot (non-admin). Local terminal Claude Code keeps local gh creds = schmug (admin) — unaffected. Accepted side effect: cloud interactive sessions become non-admin (owner does admin work locally).

Enforcement (already half-built by #295)

  • main-protection ruleset (id 14716629): keep require_code_owner_review: true, require_last_push_approval: false, required checks = ["check"] only, bypass_actors = repository admin role / bypass_mode: always. Do NOT add dmarcheck-bot to bypass_actors. Do NOT re-add require_last_push_approval or CodeQL required contexts (see issue context: those deadlocked all merges on 2026-05-16).
  • .github/CODEOWNERS (merged via security: add CODEOWNERS human-review gate for autonomous-routine PRs #295) becomes live-enforced once the author is non-admin.

Behavior matrix (intended end state)

PR touches Required to merge Outcome
Only non-CODEOWNERS paths green check Bot PR merges autonomously (intended #295 autonomy)
Any CODEOWNERS path (.github/**, package.json, package-lock.json, wrangler.toml, SECURITY.md, CLAUDE.md, .claude/settings.json, src/index.ts, src/rate-limit.ts, src/analyzers/mta-sts.ts, src/analyzers/security-txt.ts, src/db/) green check + @schmug review BLOCKED until @schmug approves in the morning sweep (legitimate — schmug is not the author)

schmug admin + bypass_mode: always is retained as the by-design owner escape hatch for the sweep. Bot keeps the default claude/* push restriction (do not enable unrestricted branch pushes for it).

Pointers

  • PR security: add CODEOWNERS human-review gate for autonomous-routine PRs #295 (merged 2026-05-16) — threat model, .github/CODEOWNERS, CLAUDE.md security posture. This issue is the deferred "Bot-identity split — owner action."
  • Ruleset: gh api repos/schmug/dmarcheck/rulesets/14716629 (classic branches/main/protection is 404 — it's a ruleset).
  • CLAUDE.md "Security" section — update to document the final identity model.
  • Memory/context: require_last_push_approval + CodeQL required contexts were removed 2026-05-16 because they deadlocked all merges; must not regress.

Constraints

  • dmarcheck-bot must NOT have admin and must NOT appear in ruleset bypass_actors, or the gate is moot.
  • @schmug (CODEOWNERS owner) must retain write access for the human sweep.
  • Do not re-introduce require_last_push_approval (non-path-scopable; breaks single-maintainer sweep) or the Analyze (actions) / Analyze (javascript-typescript) required contexts (NEUTRAL on Dependabot → deadlock).
  • Keep autonomous auto-merge for non-sensitive paths (intended security: add CODEOWNERS human-review gate for autonomous-routine PRs #295 behavior); only CODEOWNERS paths require human approval.
  • Owner-only manual steps (cannot be automated): creating the GitHub account and repointing the claude.ai GitHub connection.

Acceptance criteria

  • dmarcheck-bot exists, 2FA on, added as write (not admin) collaborator on schmug/dmarcheck.
  • dmarcheck-bot is absent from main-protection bypass_actors; ruleset still: require_code_owner_review:true, require_last_push_approval:false, required checks ["check"].
  • claude.ai GitHub connection repointed to dmarcheck-bot; a routine PR is authored by dmarcheck-bot, not schmug.
  • Verification: a throwaway dmarcheck-bot PR touching a CODEOWNERS path shows mergeStateStatus: BLOCKED until @schmug approves, then becomes mergeable.
  • Verification: a dmarcheck-bot PR touching only non-CODEOWNERS paths auto-merges on green check.
  • Verification: local terminal Claude Code still operates as schmug (admin) — gh api user --jq .login = schmug locally.
  • CLAUDE.md Security section updated to describe the final two-identity model (local=schmug admin, cloud=dmarcheck-bot non-admin) and the do-not-regress ruleset invariants.

Out of scope / follow-ups

  • Known residual (file as separate follow-up): CODEOWNERS is intentionally narrow, so a malicious-issue-driven PR touching only non-CODEOWNERS paths (e.g. a new exfiltrating module under src/analyzers/ other than the two listed, or src/orchestrator.ts) still auto-merges. Recommend a follow-up to broaden CODEOWNERS to src/analyzers/** + src/orchestrator.ts + src/shared/scoring.ts, weighed against autonomy. Not solved here — security: add CODEOWNERS human-review gate for autonomous-routine PRs #295 deliberately chose narrow scope.
  • Changing the routine's issue-selection / prompt-scoring heuristic.
  • Any runtime/app code change.
  • Migrating off Claude Code routines to GitHub Actions (alternative architecture B; rejected to preserve the routine+sweep workflow).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions