# One scoped API key per coding agent

> An API key per coding agent, scoped and set to expire, keeps a leak or a runaway loop traceable and revocable. Set it up on GitHub and on Latchkey.

Source: https://latchkey.dev/learn/agent-ci/scoped-api-key-per-coding-agent  
Updated: 2026-11-12

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.

```Token template URL (line breaks for legibility)
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=read
```

> Template parameters and the 1 to 366 day range from Managing your personal access tokens; the Actions write requirement from Permissions required for fine-grained personal access tokens; the Checks limitation from `gh run watch --help` in gh 2.102.0. All read 2026-11-12.

## GitHub 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`.

```.github/workflows/agent.yml
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 }}
```

> One-hour expiry and narrowing rules from Generating an installation access token for a GitHub App; inputs from action.yml at v3.3.0, tag SHA from `git ls-remote`. Read 2026-11-12.

## 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 |

> Presets, scopes and expiry options from latchkey.dev/documentation/connect-your-ai-agent and security-and-permissions, read 2026-11-12.

## 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.

```Terminal
# 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:read
```

> The probe and its pass message are from SKILL.md in @latchkeydev/cli 0.16.0, read 2026-11-12.

## See 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](/learn/agent-ci/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.

## FAQ

### Should I use a GitHub App or a personal access token for automation?

For long-lived automation, an App: its installation tokens expire after an hour, carry the App's identity rather than a person's, and can be narrowed per token. A fine-grained PAT is simpler to create and fits a single person's agent, but runs it starts are attributed to that person.

### How long does a GitHub App installation token last?

One hour. The token response includes the time it expires, and the Octokit SDKs regenerate it automatically when it does.

### Can a fine-grained personal access token trigger a workflow?

Yes, if it has the Actions repository permission at write on that repository, which is what the workflow dispatch endpoint requires. The workflow must also declare `on: workflow_dispatch`.

## References

- [Managing your personal access tokens, docs.github.com (read 2026-11-12)](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens)
- [Generating an installation access token for a GitHub App](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-an-installation-access-token-for-a-github-app)
- [actions/create-github-app-token](https://github.com/actions/create-github-app-token)
- [Latchkey: connect your AI agent (keys, presets, expiry)](https://latchkey.dev/documentation/connect-your-ai-agent)
- [Latchkey: security and permissions](https://latchkey.dev/documentation/security-and-permissions)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
