One scoped API key per coding agent
An API key per coding agent, scoped to the repositories and actions its job needs and set to expire, means one leak or one runaway loop is traced and revoked without touching anything else. On GitHub that means a fine-grained personal access token or a GitHub App installation token; on Latchkey it is one API key per agent, created from an access preset with an expiry.
The default is one token pasted into every tool: the CLI on the laptop, the agent in the editor, the bot in CI. It works until something goes wrong, and then nobody can say which of them ran the job, and revoking the token breaks all of them at once.
One key per agent fixes both. Before you mint one, answer four questions for that agent: what may it read, what may it start, what may it spend, and when should it stop working. The rest of this page is where each answer goes on GitHub and on Latchkey.

GitHub option 1: a fine-grained personal access token
A fine-grained token is limited to resources owned by a single user or organization, can be limited further to specific repositories, and carries specific permissions rather than broad scopes. Organization owners can require approval before one works. To dispatch a workflow, the endpoint needs the Actions repository permission at write.
GitHub lets you share a token template as a link with the fields filled in. expires_in takes 1 to 366 days or none; infinite lifetimes are allowed unless an organization or enterprise policy blocks them. Mint one link per agent role and keep it in your runbook.
Two catches. A token acts with its owner's identity, capped by its permissions, so a run it starts reads as yours. And fine-grained tokens cannot call the Checks API, which is why gh run watch says in its own help that it does not support them.
https://github.com/settings/personal-access-tokens/new
?name=agent-claude-ci
&description=Dispatch+and+read+CI+for+one+repository
&target_name=acme
&expires_in=30
&actions=write
&contents=readGitHub option 2: a GitHub App installation token
An App gives the agent an identity of its own, and its installation tokens expire after one hour. When you mint one you can narrow it to named repositories and to a subset of the App's permissions; it can never exceed what the installation was granted.
Inside a workflow, actions/create-github-app-token does the minting. Each permission is its own permission-<name> input, and the README recommends listing them explicitly because by default the token inherits every permission of the installation. The token is revoked when the job completes unless you set skip-token-revoke.
steps:
- id: agent-token
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
with:
client-id: ${{ vars.AGENT_APP_CLIENT_ID }}
private-key: ${{ secrets.AGENT_APP_PRIVATE_KEY }}
repositories: api
permission-actions: write
permission-contents: read
- run: gh workflow run agent-check.yml --ref "$GITHUB_REF_NAME"
env:
GH_TOKEN: ${{ steps.agent-token.outputs.token }}Which GitHub credential fits which agent
Pick by how long the agent lives and whose name its runs should carry.
| Credential | Lifetime | Acts as | Watch out for |
|---|---|---|---|
GITHUB_TOKEN | Until the job finishes, or its maximum lifetime | The workflow | Its events start no run, except dispatch events and approval-required pull_request runs |
| Fine-grained PAT | 1 to 366 days, or none if policy allows | The human who made it | No Checks API, so no gh run watch |
| App installation token | 1 hour | The App | Inherits every installation permission unless narrowed |
Latchkey: one key per agent, from a preset
Latchkey keys are minted in Settings, API keys and agents, by owners and admins. Name the key after where it will live, pick an expiry (Never is the default; 30, 60 or 90 days and 1 year are the others), and pick its access. Read-only is the default. Every write is a separate opt-in fixed at creation: an existing key cannot be upgraded, so a new need means a new key.
Keys are workspace-scoped, shown in full once, stored hashed, and revocable at any time; a revoked key loses access within a minute and stays listed for audit.
| Preset | Adds | A leaked key can additionally |
|---|---|---|
| Read-only (default) | mcp:read | Read CI failure and run data, including log excerpts |
| CLI runner | jobs:run (with jobs:read) | Run commands on billable runners |
| Dispatch | mcp:dispatch | Trigger any dispatchable GitHub Actions workflow in monitored repositories |
| CLI runner and secrets | secrets:write | Set or replace any workspace or repository secret, never read one back |
| Approve runs | jobs:approve | Approve a waiting run for any environment |
| Migration pull requests | migration:apply | Open a migration pull request |
| Full access | Every scope, including workflows:run | All of the above, and start any Latchkey CI workflow |
Match the preset to the agent
A triage agent that reads failures needs Read-only. An agent that verifies its own edits needs CLI runner. An agent that re-runs a GitHub Actions workflow after a fix needs Dispatch, and nothing more. Keep Full access away from agents: Latchkey's docs point out that a key with jobs:approve held by an agent that can also push can approve the runs of its own changes, with no other person deciding.
Hand the key to the agent through its environment rather than a file. The CLI reads LATCHKEY_TOKEN, and a read-only status probe checks the key and its scope without creating anything.
# in the shell that starts the agent, one key per agent
export LATCHKEY_TOKEN=lk_live_... # key named "claude-code on build box", CLI runner, 30 days
latchkey status cli-00000000-0000-0000-0000-000000000000
# "Job cli-... was not found in this workspace." means the key works and carries jobs:readSee what each key ran
Attribution is the reason for all of this, so check it. Settings has an Agent activity tab that shows what each agent's key ran. In the terminal, latchkey list prints a KEY column naming the key that started each job, and the API keys table shows when each key was last used, flagging one that has gone unused for 30 days.
A key has no spending budget of its own. Bound an agent's spend through its work instead, with a batch budget named for the agent, as cap agent CI spend shows. When an agent starts getting 401s, the key was revoked or reached its expiry: mint a new one rather than widening an old one.
Documentation, not a captured session
No recorded run sits behind this page, and the repository's evidence files have nothing for it. The GitHub rows come from docs.github.com (personal access tokens, installation tokens, GITHUB_TOKEN) and actions/create-github-app-token at v3.3.0; the Latchkey rows from its connect-your-agent and security pages and the SKILL.md in CLI 0.16.0, all read 2026-11-12. A key minted for a demonstration would prove that one key works, not what a preset allows.
Key takeaways
- One key per agent makes "which agent ran this" answerable and revocation local.
- Fine-grained PATs carry your identity and cannot call the Checks API; App tokens carry the App's and last one hour.
- Latchkey keys are read-only by default; each write is chosen at creation and cannot be added later.
- Keys have no budget of their own: bound spend with a batch named for the agent.
Frequently asked questions
Should I use a GitHub App or a personal access token for automation?
How long does a GitHub App installation token last?
Can a fine-grained personal access token trigger a workflow?
on: workflow_dispatch.