Tokens and permissions

Scope credentials to the capability that uses them.

Every token-bearing surface in Goobers — the repo token, a per-capability credentials: grant, daemonIdentity, a webhook secret — is a reference, never an inline value: token.env, token.file, token.keychain, or token.store. Capability admission only holds as a real security boundary if the underlying tokens are actually scoped narrowly. A single all-powerful PAT reused for every capability grant makes that boundary a formality.

Prefer GitHub's fine-grained personal access tokens over classic PATs. Classic PATs grant access to every repo the user can see under a coarse scope; a fine-grained PAT is scoped to specific repositories and specific permissions.

One token ref per capability, not one token for everything

Mint a separate fine-grained PAT per capability class your gaggle's workflows actually use, and register each as its own named ref in instance.yaml:

Capability Recommended fine-grained permissions Notes
github:issues:read Issues: Read-only Backlog polling, triage stages.
github:issues:write Issues: Read and write Create, claim, comment, ordinary-label, close.
github:milestones:write Issues: Read and write Assign an existing milestone to an issue — keep roadmap mutation out of stages that only perform ordinary issue writes.
github:issues:approve Issues: Read and write Applies the goobers:approved trust decision. Keep out of workflow stages unless self-approval is intentional.
provider:pr:write Pull requests: Read and write, Commit statuses: Read-only, Actions: Read Provider-neutral pull-request stages such as ci-poll. GitHub's fine-grained PAT UI has no separate Checks permission, so a private repo whose CI is entirely GitHub Actions-based is otherwise unreadable on this token type — also granting Actions: Read lets CI-status checks fall back to GET /actions/runs on that specific 403.
github:pr:read Pull requests: Read-only Forge merge-inventory comparison. Explicit credential only — it never inherits a write token or authorizes any mutation.
github:pr:write Pull requests: Read and write, Contents: Read and write GitHub-specific stages that open or update PRs.
github:pr:review Pull requests: Read and write Submit native approve/request-changes reviews. Source this from a different GitHub identity than github:pr:write for goober-authored PRs — GitHub forbids self-approval.
provider:ci:cancel Actions: Read and write Cancel a bounded set of pending GitHub Actions runs, only after re-checking the open PR and its exact reviewed head SHA. Providers without a safe cancellation API report unsupported without changing the published verdict.
repo:push Contents: Read and write Branch + commit + push. The broadest local-tier grant; scope it to the exact target repo(s), never an org-wide token.
repo:read Contents: Read-only A read-only worktree of the stage's own target repo, with no push — the capability the work-nomination lane uses. There is no repo:clone.
contents:read Contents: Read-only Clone/fetch of a separate reference repository declared as a gaggle additionalRepos entry. Routed per repo as a repo-qualified grant key (contents:read@owner/name); used only at provision time and never handed a push path.
github:branch:delete Contents: Read and write Delete a remote branch ref after its PR merges (and no open PR uses it as a base), or when a terminal run's pushed branch never became a PR.
github:pr:merge Pull requests: Read and write, Contents: Read and write Merge or merge-queue a pull request. Grant it only to merge-review's landing stage — the decider/executor split is the point. Never add it to implementation or pr-remediation.
github:code-scanning:read Code scanning alerts: Read-only Read default-branch CodeQL and other SARIF-uploading tools' alerts for scheduled work nomination. Strictly read-only: it authorizes no issue, pull-request, or contents write. Grant it alongside a separate github:issues:write only on the stage that actually files nominations, never on the query stage itself.
github:dependabot-alerts:read Dependabot alerts: Read-only Read vulnerable-dependency alerts for scheduled work nomination. A separate GitHub permission from code scanning, and a separate capability here, so a workflow that only nominates dependency work never receives code-scanning access.
telemetry:read (no GitHub permission) Read the local telemetry rollup and named host-governed external connectors. Backed by connector configuration, not a GitHub PAT.
journal:read (no GitHub permission) Read-only, digest-verified access to another run's journal. Local filesystem authority, not a GitHub PAT.
configrepo:read Contents: Read-only Runner-only access to the workflow-config repo — configure only through workflowSource.token; stages cannot declare or source it through credentials.
configrepo:write Contents: Read and write, Pull requests: Read and write (limited to the config repository) Stage-declarable write sibling of configrepo:read: lets a tutor stage push a branch and open a PR in the workflowSource repository. With a github-app workflowSource the daemon mints it automatically, down-scoped to that repository; with a token-authed workflowSource provision a dedicated fine-grained PAT restricted to the config repository only. Never reuse the product-repo token.
agent:model Stored copilot or claude CLI sign-in, or a headless secret: Copilot Requests: Read-only (Account permissions) for copilot; a real sk-ant-... Anthropic API key or Claude Code subscription OAuth token for claude-code Agent harness model authentication for agentic stages. An existing per-user CLI sign-in is the local default on either harness; a configured headless secret is injected as COPILOT_GITHUB_TOKEN (copilot) or ANTHROPIC_API_KEY/CLAUDE_CODE_OAUTH_TOKEN (claude-code). Only one unscoped agent:model grant exists per instance — an instance running goobers on more than one harness adds harness: to each grant instead.
Repository access: select "Only select repositories." List exactly the gaggle's target repo(s) when minting a fine-grained PAT — never "All repositories."
repos:
  - provider: github
    owner: your-org
    name: your-repo
    token:
      env: GOOBERS_GITHUB_REPO_TOKEN # Contents: read-only
credentials:
  - capability: github:issues:write
    token:
      env: GOOBERS_GITHUB_ISSUES_TOKEN # Issues: read and write
  - capability: github:pr:write
    token:
      env: GOOBERS_GITHUB_PR_TOKEN   # PR/Contents: read-write; commit statuses: read-only
  - capability: repo:push
    token:
      env: GOOBERS_GITHUB_PUSH_TOKEN # Contents: read and write
  - capability: agent:model
    token:
      env: GOOBERS_COPILOT_TOKEN     # Copilot Requests: read-only; no repo access

Each credentials: entry sources one capability from its own token ref; an entry for a capability the repo token would otherwise back overrides it, so an issues-only stage never receives a token that also carries code or PR authority. Omit overrides for capabilities no selected workflow declares.

daemonIdentity: one distinct bot identity for authored PRs, reviews, and merges

One instance.yaml block backs the whole daemon-mutation capability set — repo:push, github:issues:write, github:pr:write, github:pr:review, github:branch:delete, github:pr:merge — with one distinct machine-account identity, instead of repeating a credentials: entry per capability:

daemonIdentity:
  kind: pat
  token:
    env: DAEMON_GITHUB_TOKEN

Mint the machine account's fine-grained PAT with the union of the permissions those capabilities need, never the operator's own token. Every daemon-authored PR, review, and merge then carries that account's login instead of being indistinguishable from the operator's own manual activity. An explicit credentials: entry for any one of the six capabilities still overrides daemonIdentity for that capability alone. kind: github-app reuses the same installation-token minting a repo's own auth.kind: github-app uses.

Why a second identity exists at all: GitHub forbids self-approval. A verdict posts as a native GitHub review only when the reviewing identity differs from the PR's author; on a single-identity instance every daemon-authored PR hits GitHub's self-review rejection and degrades to a documented comment/label handoff that the merge stage reads instead.

Agentic stages: stored login or the agent:model token

The permissions above cover the ordinary GitHub API. They are not sufficient for an agentic stage (any stage whose goober uses the Copilot, Claude Code, or Codex harness) — the Copilot CLI authenticates to its model backend independently of repository credentials. For an interactive local daemon, sign in with copilot once; Goobers passes only the profile-location variables needed to find that stored session.

For a headless Windows Service, CI runner, or dedicated account without a stored session, configure a separate fine-grained PAT with Copilot Requests: Read-only — an account-level permission grantable only on a personal fine-grained PAT, never one scoped to an organization's repositories. When the target repo lives in an org, the agent:model token is necessarily a different token from the repo credential, with no repository access at all. An org owner must approve personal fine-grained PATs before they can access org repos, so budget for that approval step.

Also export the model token to the Goobers process. The harness auth preflight authenticates a clean CI/service profile from an ambient COPILOT_GITHUB_TOKEN — a token.env or token.file reference on the agent:model capability alone only reaches the eventual agentic stage, not daemon startup:
export COPILOT_GITHUB_TOKEN="$GOOBERS_COPILOT_TOKEN"
A mis-scoped token then fails during preflight rather than at the first agentic stage.

Verify harness availability before a live run with goobers validate --check-harness.

Codex is the one harness that doesn't use a stored CLI login at all: the agent:model capability for it is an OpenAI API key (CODEX_API_KEY), not a GitHub token, and the ChatGPT-login codex login flow isn't a supported auth path for Goobers. See the grant semantics and the harness enum — harness: must be one of copilot, claude-code, or codex.

Token storage options

Every token-bearing configuration surface uses the same reference shape. Choose exactly one source:

Source Best fit Tradeoffs
env: NAME Ephemeral local runs and CI secret injection Most portable and simple, but present in the daemon's process environment; an already-running process does not pick up a changed shell or service definition.
file: /path Headless services with a mounted secret file Portable and easy to rotate in place. Goobers fails closed unless the file is private to its owner (Unix mode 0600, or an owner-only Windows DACL).
keychain: SERVICE Interactive or launchd-managed macOS runners Encrypted at rest by macOS and read for each resolution rather than retained. macOS-only; the daemon's user session must be able to unlock the login keychain without an interactive prompt.
store: STORE/SECRET Team and cloud deployments using a declared external secret store Keeps secret material out of local configuration and disk, with rotation managed centrally. Requires store identity/network configuration and uses the resolver's bounded cache rather than fetching on every use.

Create one macOS Keychain item per capability, using a unique service name:

security add-generic-password -U -a "$USER" \
  -s "goobers/github-issues" -w

Then reference only the service name in instance.yaml:

credentials:
  - capability: github:issues:write
    token:
      keychain: goobers/github-issues

Beyond a PAT

GitHub App installation tokens (auth.kind: github-app) mint a short-lived token per run instead of holding a long-lived PAT — no credential to rotate, escrow, or leak, and activity attributes to the App's own identity. See the product's token-scopes guide .

Azure DevOps repos authenticate through auth.kind: azure-cli for local development, workload-identity or managed-identity for unattended Kubernetes/Azure hosts, or a plain pat. See the ADO authentication guide .

External secret stores (Azure Key Vault today) let a token ref resolve through store: storeName/secretName instead of env/file, with rotation managed centrally. An instance that declares none behaves exactly as before.

Product source: docs/guides/github-token-scopes.md and docs/guides/ado-authentication.md.