Setting Codespaces Secrets and Machine Types per Repo
A codespace starts, but npm ci fails with npm ERR! 401 Unauthorized - GET https://npm.pkg.github.com/@acme%2fui because NPM_TOKEN is empty; meanwhile another team's repository somehow has access to the production analytics key, and the monthly bill shows 32-core machines idling over the weekend. Secrets and machine sizing are the two settings that most often go wrong when a team moves from a pilot to real use of Codespaces. This page sets both up deliberately, as part of cloud development environments for onboarding.
Codespaces secrets are separate from Actions and Dependabot secrets, even when they share a name. That separation is the root of most "I set the secret but it's not there" reports.
Diagnostic
Inside the failing codespace, check which expected variables are present, then list what is configured at each level from a terminal with admin rights:
#!/usr/bin/env bash
set -euo pipefail
for v in NPM_TOKEN SENTRY_DSN STRIPE_TEST_KEY; do
val="${!v:-}"
if [ -n "$val" ]; then echo "$v: set (${#val} chars)"; else echo "$v: MISSING"; fi
done
gh secret list --app codespaces --repo acme/shop
gh secret list --app codespaces --org acme
gh secret list --app actions --repo acme/shop
Expected bad output:
NPM_TOKEN: MISSING
SENTRY_DSN: set (95 chars)
STRIPE_TEST_KEY: MISSING
NAME UPDATED
SENTRY_DSN 2026-07-02
NAME UPDATED VISIBILITY
ANALYTICS_KEY 2026-05-11 all
NAME UPDATED
NPM_TOKEN 2026-08-20
NPM_TOKEN exists only as an Actions secret, so codespaces never receive it. And an organisation-level Codespaces secret with visibility all is injected into every repository's codespaces, including ones that have no business holding it.
Root cause
GitHub keeps secrets per "app": Actions, Codespaces and Dependabot each have their own store. A secret created for Actions is available to workflow runs only; the Codespaces store is what gets injected as environment variables when a codespace starts. Within Codespaces there are three levels — user, repository and organisation — and organisation secrets have a visibility setting (all, private, or selected repositories). Defaults tend towards convenience: organisation admins pick all to make a pilot work, and every repository then receives the secret. Separately, secrets are injected at creation and on restart, so adding a secret while a codespace is running has no effect until it restarts.
Machine sizing has a similar shape. Without a policy, any developer can pick any machine type the organisation allows, and without an idle timeout policy the default of 30 minutes can be raised per user. Prebuilds, which run on their own machines, add a separate line to the bill that many teams do not notice until the first invoice.
Resolution
- Create secrets in the Codespaces store, scoped as narrowly as possible:
#!/usr/bin/env bash
set -euo pipefail
gh secret set NPM_TOKEN --app codespaces --repo acme/shop --body "$(op read 'op://dev/github-packages/read-token')"
gh secret set STRIPE_TEST_KEY --app codespaces --repo acme/shop --body "$(op read 'op://dev/stripe/test-secret-key')"
gh secret set ANALYTICS_KEY --app codespaces --org acme --visibility selected --repos acme/dashboard
gh secret list --app codespaces --repo acme/shop
Use read-only, development-scoped credentials: a packages token that can only read, Stripe test-mode keys, sandbox API keys. Nothing that can touch production data belongs in a CDE secret.
- Declare which secrets the repository expects in
devcontainer.json, so Codespaces prompts developers for user-level values on creation and documents them in one place:
{
"secrets": {
"NPM_TOKEN": { "description": "Read-only token for GitHub Packages", "documentationUrl": "https://docs.acme.dev/onboarding/npm" },
"STRIPE_TEST_KEY": { "description": "Stripe test-mode secret key" }
}
}
Restart running codespaces after adding secrets:
gh codespace stop -c <name>and start again. Rebuilding is not required.Restrict machine types and idle time with an organisation policy. In Organisation settings → Codespaces → Policies, create a policy for the repository that limits machine types to those in
hostRequirementsand caps idle timeout and retention:
#!/usr/bin/env bash
set -euo pipefail
gh api /repos/acme/shop/codespaces/machines --jq '.machines[] | "\(.name)\t\(.cpus) cpu\t\(.memory_in_bytes/1073741824|floor) GB"'
gh api -X GET /orgs/acme/codespaces --jq '.codespaces[] | "\(.machine.name)\t\(.state)\t\(.last_used_at)\t\(.owner.login)"' | sort | head -20
Set the repository's hostRequirements to the smallest machine that runs the stack comfortably — often 4 cores and 16 GB — and let the policy prevent larger ones except for repositories that genuinely need them.
Expected output
NPM_TOKEN: set (40 chars)
SENTRY_DSN: set (95 chars)
STRIPE_TEST_KEY: set (107 chars)
$ npm ci
added 1412 packages in 38s
$ gh api -X GET /orgs/acme/codespaces --jq '[.codespaces[].machine.name] | group_by(.) | map("\(.[0]): \(length)")[]'
standardLinux32gb: 14
Every expected secret is present, the private package installs, and all codespaces for the repository run on the allowed machine type. The diagnostic prints only the length of each value, never the value itself, so its output is safe to paste into a support channel when something is still missing.
The organisation-level ANALYTICS_KEY now appears only in the dashboard repository's codespaces. Verify that by starting a codespace in an unrelated repository and checking the variable is absent — scoping mistakes are invisible from the repository that is configured correctly.
Prevention
Audit organisation secret visibility quarterly. List organisation Codespaces secrets with visibility
alland justify each one; most should beselected.Keep the expected-secrets list in
devcontainer.jsonand check it inmake doctor, which prints exactly which variable is missing and where to get it.Alert on cost anomalies. A weekly job that lists running codespaces older than a day, or machines larger than policy, catches forgotten workspaces before they dominate the bill.
Platform caveats
All clients: secrets are environment variables inside the codespace, visible to any process the developer runs, including extensions. Treat them as development credentials that a compromised extension could read, and scope them accordingly.
Prebuilds: secrets are available to prebuilds only if configured for them; do not rely on user secrets during
onCreateCommand. Keep private-package access in repository secrets if prebuilds need it.
Apple Silicon (ARM64) developers: machine types are x86_64 regardless of the client; size by the stack's needs, not by the laptop's specs.
Rollback
Delete or narrow secrets and remove the policy to return to the previous state; running codespaces keep injected values until restarted:
#!/usr/bin/env bash
set -euo pipefail
gh secret delete STRIPE_TEST_KEY --app codespaces --repo acme/shop
gh secret set ANALYTICS_KEY --app codespaces --org acme --visibility private --body "$(op read 'op://dev/analytics/key')"
Frequently Asked Questions
Why is my Actions secret not available in Codespaces?
Actions and Codespaces have separate secret stores. Create the secret again with gh secret set NAME --app codespaces at the repository, organisation or user level, then restart the codespace.
Do I need to rebuild a codespace after adding a secret?
No. Secrets are injected when the codespace starts, so stopping and starting it is enough. A rebuild also works but takes longer.
Should developers use their own credentials or shared ones?
Use personal (user-level) secrets for anything tied to an identity — cloud CLI access, personal API keys. Use repository secrets for shared, low-privilege development credentials such as a read-only package token or test-mode keys.
How do we stop people choosing large machines?
Create an organisation Codespaces policy that restricts machine types for the repository, and set hostRequirements in devcontainer.json so the default choice is already the right size.