# Credentials (/docs/security/credentials)



| What                      | Where it lives       | Reaches a workspace as                       |
| ------------------------- | -------------------- | -------------------------------------------- |
| Proxmox API token         | `.env`               | never leaves the controller                  |
| GitHub App key            | file on disk, `0600` | a repo-scoped token, renewed hourly          |
| Claude subscription token | `.env`               | `~/.config/agent-env`, sourced by the runner |

Both workspace credentials arrive **on stdin**, never as arguments, so neither appears in the
process list. Git reads its credential from a store rather than a URL, because git repeats the
remote it was using in its errors and those reach the timeline the UI renders.

`ssh` does not preserve argument boundaries — it joins the command and the remote shell re-splits
it — so everything sent is quoted before it leaves.

## Proxmox [#proxmox]

Use a dedicated Proxmox user and privilege-separated API token. **The controller holds it and
nothing else does**: it never reaches a workspace, and no agent can ask for it.

```text
VM.Audit
VM.Clone
VM.Allocate
VM.PowerMgmt
VM.Config.Options
VM.Config.CPU
VM.Config.Memory
VM.Config.Network
Datastore.AllocateSpace
SDN.Use
```

Add `VM.Config.Disk` only if the controller changes disk configuration.

Scope permissions to the golden template (audit and clone), the pre-created
`disposable-workspaces` pool, the clone storage, and the specific bridge or VNet. Do not grant
broad administrator, console, permission-management, or host-modification privileges.

## The template [#the-template]

Do not bake secrets into the LXC template.

| Safe                               | Unsafe                                      |
| ---------------------------------- | ------------------------------------------- |
| Controller SSH public key          | Proxmox API tokens                          |
| SSH user CA public key             | GitHub access tokens or private deploy keys |
| SSH host CA public key             | Model-provider API keys                     |
| Public TLS trust roots             | Claude, Codex, or GitHub login state        |
| Non-secret bootstrap configuration |                                             |

LXC cloning does not provide QEMU-style cloud-init, and Proxmox does not expose a general REST
`pct exec` or `pct push`. The initial bootstrap channel must therefore be prepared in the template
or supplied through a separately controlled Proxmox-host mechanism.

Bake a dedicated controller public key for the non-root `agent` account. Keep its private key only
on the hosted controller.

## Git [#git]

GitHub App installation tokens, minted per repository and lasting an hour.

1. Mint a token scoped to that repository alone.
2. Store it with git's `credential.helper store`, written from **stdin** so it is never an argument
   and never appears in `ps` on the workspace.
3. Clone over HTTPS from a credential-free URL and let git read the stored value itself.
4. Replace the stored credential before the hour is up, for as long as the workspace lives.

**The token is stored rather than used once and deleted**, which is a deliberate departure from the
obvious design. The agent pushes later under its own steam, and a workspace outlives an
installation token several times over. A refresh pass replaces it on a living workspace without
touching its repository.

<Callout type="warn">
  Never put the token in the URL. git repeats the remote it was using in its error text, and that
  text reaches the workspace timeline the UI renders. Everything git prints is scrubbed of the token
  at the boundary too, because one of those paths will be missed eventually.
</Callout>

Avoid forwarding the user's general SSH agent into an autonomous workspace. It grants broader
signing and repository access than the task usually needs.

## Model provider [#model-provider]

Provider authentication is the largest unavoidable trade-off here, and the one that was settled
least comfortably.

**What is used: a Claude subscription OAuth token**, held in `.env` on the controller and written
into each workspace as `~/.config/agent-env`, which the runner sources explicitly. It arrives over
stdin, never as an argument. An API key was available and was deliberately not chosen.

It is the fourth option on the list below, and it is chosen knowingly: this is a single-operator
deployment, the token is reusable rather than task-scoped, and any workspace that gets it can spend
against the subscription. The container being disposable is what bounds that, not the credential.

Better, in order, if this ever stops being a single-operator tool:

1. Short-lived, task-scoped provider credential.
2. Controller-side credential broker that exchanges a workspace identity for limited access.
3. Narrowly scoped API key injected as a mode `0600` file.
4. Reusable login state, which is what is in use.

Track which credential classes were injected, never their values. Remove credential files during
destruction even though deleting the LXC also removes its filesystem.
