Tokens and credentials¶
The agent posts under a real GitHub account and spends a real metered LLM
allowance. Both are credentials, and neither is in config.yaml.
🔑 The GitHub token¶
Read from the GITHUB_TOKEN environment variable, and only from there.
1 | |
Never from config.yaml. The config file names repositories and accounts,
is meant to be readable, and is the thing you would paste into an issue when
asking for help. A credential in it would leak the first time anyone did that.
config validate therefore needs no token at all — it answers a question about
the file, not about GitHub.
What it needs: a fine-grained personal access token scoped to the one repository the config names, with exactly three permissions:
| Permission | Access | Why |
|---|---|---|
| Pull requests | Read and write | Read lists the open pull requests and the inline review comments; write puts the 👀 on an inline review comment that mentioned the agent. |
| Issues | Read and write | A pull request's conversation comments are issue comments in the REST API. Read is the @claude feed; write posts the review or description, the notices, and the 👀 on a conversation comment or the pull request. |
| Metadata | Read only | Mandatory on every fine-grained token, and what host check reads the repository with. |
Nothing else. In particular Contents is not needed: the checkout fetches
the repository over anonymous https and no credential ever reaches git
(see WORKSPACE.md), so a token that can write code is a blast
radius the agent has no use for.
A classic PAT with repo also works, and is coarser than all of the above put
together. Prefer the fine-grained one.
Scope it to that repository. A token that can reach more than the agent is configured for is a token whose blast radius is larger than the agent's.
What host check can and cannot tell you¶
pr-review-agent host check reads GET /repos/{owner}/{name} and reports on
permissions.push. Read its answer with the limit in mind: push is
contents: write, which is what a classic repo token carries and what the
list above deliberately leaves out. So a correctly scoped fine-grained token
reports push: false, and the check says so as a caution naming the three
permissions rather than as a failure.
That is the honest limit of a read-only pre-flight. GitHub publishes no per-resource permission breakdown on that response, and the only way to be certain a token may comment is to post a comment — which is not something a pre-flight check may do to somebody's pull request. What the check does still catch outright is a token that cannot read the repository at all.
What happens to it. It is passed as a Bearer header and nothing else. It
is never logged, never printed by the pre-flight checks — those report what
happened, not what was sent — and under systemd it reaches the process through
EnvironmentFile= rather than ExecStart=, so it does not appear in ps, in
systemctl show or in systemctl cat.
📄 The token file under systemd¶
service install creates an empty token.env at mode 0600 and never
overwrites it again — not even under --force, because by the second install it
holds a real credential.
1 | |
One line. No quotes, no export:
1 | |
👥 One token per repository¶
When several daemons share a budget — one process per repository, all pointing
at one store.path — each keeps its own token in its own file:
| Single | Several repositories | |
|---|---|---|
| token file | ~/.config/pr-review-agent/token.env |
~/.config/pr-review-agent/<instance>/token.env |
| unit | pr-review-agent.service |
pr-review-agent@<instance>.service |
This is the whole reason the design is a process per repository rather than one process with a list. Isolation is the operating system's to enforce: a daemon that never holds another repository's token cannot post with it, however it misbehaves. Merging the tokens into one credential would give that back — one compromised or buggy instance could then write anywhere the shared token reaches.
Two rules follow, and neither is enforced by code:
- Do not point two units at one
token.env. The files are per-instance and mode0600; keep them that way. - Do not reuse one PAT across instances. It defeats the isolation even when the files are separate.
The queue is scoped to its repository so that a daemon only ever claims its own work — see QUEUE.md — but that is a correctness guard, not a substitute for separate credentials.
🧍 Who may spend the allowance¶
A token decides what the agent can do. The allowlist decides whose requests it will act on, and it is a separate control:
1 2 3 | |
Allowlisting is on the numeric user id, never the login. A login can be renamed and the freed name registered by a stranger; a user id cannot be transferred.
Each repository carries its own allowlist, in its own config.yaml. Sharing a
budget does not share trust.
Pull request bodies, comment bodies and diffs are untrusted input. Nothing in them can widen what the agent is allowed to do.
🤖 The LLM credential¶
The review engine is the claude CLI, and it authenticates itself — the agent
holds no Anthropic credential and sends none. host check probes
api.anthropic.com only to prove the route exists: no API key is sent and
none is needed, so any HTTP status passes and only a transport error fails.
The engine runs with a scrubbed environment except for ANTHROPIC_*, which is
passed through if you have set it. --bare was rejected for the engine
invocation precisely because it would force ANTHROPIC_API_KEY authentication
and silently settle the billing question in favour of per-token API charges
rather than the CLI's own subscription.
What the agent does own is the spending, which is why the budget governor sits in front of every review and why several repositories sharing one plan must also share one store and one authority.
🔁 Rotating a token¶
- Issue the new PAT with the same repository scope.
- Edit the
token.envfor that instance — or export the new value if you run by hand. systemctl --user restart pr-review-agent@<instance>(orpr-review-agent.service). The token is read at startup, soSIGHUPdoes not pick up a new one.- Revoke the old PAT.
A restart is safe at any point: an in-flight review loses its lease and is retried, and the watermarks and the ledger are on disk.
🚨 If a token leaks¶
Revoke it at GitHub first — that is the only step that stops it being used.
Then issue a replacement and restart the instance. Check the account's recent
activity for anything the agent did not do: every review the agent posts is
recorded in runs, so the database is the list to compare against.
Shell history is worth a thought here. GITHUB_TOKEN=... pr-review-agent ... on
one line is convenient and lands the credential in ~/.zsh_history or
~/.bash_history in plain text. Prefer the token.env file, or a leading space
where your shell is configured to keep those out of history.