Secrets and environments

Secrets that reach only the jobs that need them

Keep registry tokens and other secrets, and settings that aren't secret, for an organization, a repository or one environment. A job gets only what it names, a run an agent set off gets no secrets until a person approves it, a cloud can trust a short-lived identity token instead of a stored key, and a build server can have a deploy key for one repository.

Three levels

An organization's, a repository's, an environment's

An organization's secrets go to all its repositories or only those you choose, a repository's to its own runs, and an environment's only to jobs with environment: set to it. A job gets just the names it lists in secrets:, as environment variables. For the same name, the environment's wins over the repository's, and the repository's over the organization's. Each holds up to 100, and the run's page lists what each job got and from where. Variables, for what isn't secret, such as a region or an address, sit at the same three levels: a job names them in vars:, a variable wins over the workflow's env: and a secret over a variable, and they're shown as set, never masked, and reach runs whose secrets are withheld too.

acme/invoices › Runs › #41

Jobs

  • test
  • publishSecrets from the repository: NPM_TOKEN
  • deploydeploys to productionSecrets from production: DEPLOY_KEYSecrets from acme: SENTRY_AUTH_TOKENVariables from production: API_URL, REGION

Write-only

Encrypted, and never read back

Each value is encrypted with AES-256-GCM under the server's SECRETS_KEY and bound to its own secret, so a copy moved elsewhere won't decrypt. It can be replaced or deleted, never read back. It leaves the control plane only when a runner takes a job that names it, and that job's log shows *** in place of the value, each of its lines, and its base64 and URL-encoded forms. Values under 4 characters aren't masked.

acme/invoices › #41 › deploy
$ curl -sfv -H "Authorization: Bearer $DEPLOY_KEY" https://deploy.acme.example/release
> Authorization: Bearer ***
< HTTP/2 202
$ printf %s "$DEPLOY_KEY" | base64
***

Kept encrypted. It reaches only the jobs that name it, and can't be read back.

Agents' runs

No secrets until a person approves

A run an agent or app set off gets no secrets and no identity tokens, so an agent can't read one by editing a workflow on its branch. Nor does a run set off by someone without write access. Its jobs that name secrets wait, and the rest run. An administrator reads the workflow and chooses Approve and run, as a person. A run at the latest commit of a branch or tag that agents can't update gets them, since a person put that commit there. Graph builds, which run the repository's setup commands, never get secrets.

acme/invoices › Runs › #42

This run comes from an agent, claude-code: it set this run off, or the change or earlier run it comes from. So its secrets and identity tokens are withheld: its jobs that use them wait until an administrator approves it, as a person, never an agent. Read the workflow at 7e4f0b12a9c3 first: once approved, it can do anything with them.

  • testpassed
  • publish secrets: [NPM_TOKEN]waiting for approval
Approve and run

Identity tokens

Cloud access without a stored key

A job with id-token: write under permissions: can ask Branchscope for an OpenID Connect token, signed with RS256 and good for 10 minutes, and trade it with AWS, Google Cloud or Azure for credentials. Its subject names the repository and the environment, such as repo:acme/invoices:environment:production, so a cloud role can trust only jobs that environment let through. Tokens follow the rules secrets do, are masked in the log, and aren't stored.

GET $BRANCHSCOPE_ID_TOKEN_REQUEST_URL?audience=sts.amazonaws.com
{
  "iss": "https://branchscope.example",
  "sub": "repo:acme/invoices:environment:production",
  "aud": "sts.amazonaws.com",
  "repository": "acme/invoices",
  "ref": "refs/tags/v1.4.0",
  "ref_type": "tag",
  "environment": "production",
  "runner_environment": "hosted"
}

An identity token for sts.amazonaws.com

CLI and audit log

Set from standard input, recorded by name

bscope secret set NAME reads the value from standard input, so it stays out of your shell history, and --org, --repos and --env say where it goes. Repository administrators set, list and delete a repository's secrets and organization owners an organization's, as do site administrators, always as people: agents and apps can't even list them, and no MCP tool touches them. The audit log records each change by the secret's name, with its kind and environment, never its value, and each run whose secrets were approved. Variables work the same way with bscope variable, and the audit log records them by name too.

~/invoices
$ bscope secret set DEPLOY_KEY --env production --kind hetzner < key.txt
Set DEPLOY_KEY for jobs deploying to production; jobs get it by naming it (secrets: [DEPLOY_KEY]).
$ bscope secret list
NAME KIND LEVEL UPDATED LAST USED
NPM_TOKEN npm repository 3 days ago 2 hours ago
SENTRY_AUTH_TOKEN other organization (acme/invoices) 9 days ago never

Deploy keys

A key for one repository, not for a person

A deploy key is an SSH key for a server or a build machine that reaches one repository and nothing else: read-only, unless an administrator allows write. A key can't be both a person's and a deploy key, nor serve two repositories. Its pushes show in activity as the deploy key's, go through the push guard and every ruleset, and the runs they set off get no secrets until an administrator approves. Remove it to stop it at once; adding and removing are on the audit log.

acme/invoices › Deploy keys
KeyAccessAddedLast used
Build serverSHA256:q3Vx…9fKcRead-onlyalice, 2 weeks ago3 hours ago
Release machineSHA256:Lm7a…2tQeRead and writebob, 4 months agoyesterday

Give each job only what it needs

Add a secret under Settings → Secrets, or an environment's on its page under Settings → Environments, and name it in the job's secrets:.