> ## Documentation Index
> Fetch the complete documentation index at: https://docs.run.cloud/llms.txt
> Use this file to discover all available pages before exploring further.

# Secrets in CI

> Inject run.cloud org secrets into CI jobs, not GitHub Actions secrets

# Secrets in CI

CI jobs use the **same secret store as sandboxes**: groups and names managed in
your run.cloud org ([Secrets](/sandboxes/secrets), or the
[dashboard](https://run.cloud/dashboard/secrets)).

They do **not** read the GitHub repository or organization **Actions secrets**
store. If a workflow references `${{ secrets.FOO }}`, that name is resolved
from run.cloud `org` secrets, not from GitHub.

## Create secrets

CLI:

```bash theme={null}
runcloud secret-group create npm --key NPM_TOKEN
# or from a file
runcloud secret-group create npm --from-dotenv npm.env
```

Dashboard: paste a `.env` or add keys under your org’s Secrets page.

Values are **write-only** in the dashboard and CLI: listing a group shows names
and kinds, not values. run.cloud does not put secret values into Check Run
summaries on purpose (it may log secret **names** for debugging). Check logs
still include the job’s **stdout and stderr** as-is, so anything your steps
print can appear in GitHub.

## Inject a whole group

Native-style list (also valid in opt-in jobs when your tooling allows unknown
job keys):

```yaml theme={null}
jobs:
  publish:
    runs-on: runcloud
    container: node:22
    secrets:
      - npm
    steps:
      - run: npm publish --access public
```

Every env secret in the `npm` group becomes an environment variable in the
sandbox for that job.

## Inject by name (`${{ secrets.NAME }}`)

GitHub-style env mapping, where the secret **name** must match the env key:

```yaml theme={null}
jobs:
  publish:
    runs-on: runcloud
    container: node:22
    env:
      NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
      CI: "true"
    steps:
      - run: npm publish --access public
```

| Form                                       | Meaning                                                |
| ------------------------------------------ | ------------------------------------------------------ |
| `NPM_TOKEN: ${{ secrets.NPM_TOKEN }}`      | Load org secret named `NPM_TOKEN` into env `NPM_TOKEN` |
| `CI: "true"`                               | Static value (no secret)                               |
| `FOO: ${{ secrets.BAR }}` with `FOO ≠ BAR` | **Not supported**; use the same name                   |

Only a single `${{ secrets.NAME }}` reference per env entry is supported (no
string interpolation around the expression).

## Groups plus names

You can combine both:

```yaml theme={null}
jobs:
  deploy:
    runs-on: runcloud
    container: ghcr.io/example/deploy:latest
    secrets:
      - aws
    env:
      DEPLOY_HOOK: ${{ secrets.DEPLOY_HOOK }}
      ENVIRONMENT: production
    steps:
      - run: ./scripts/deploy.sh
```

Static `env` values are applied **after** secrets, so they can override a
secret of the same name if you ever need that.

## Missing secrets

If a job requests a group or name that does not exist in the org bound to the
GitHub installation, the Check Run **fails** with a clear message (fail closed).
Create the secret in the correct org, then re-run the check.

## Security notes

* Secrets are injected only into the guest environment for that job’s sandbox.
* Installation tokens used to clone the repo are not written into Check output.
* Prefer same-repository PRs; fork workflows are restricted in v1 for the same
  reasons as other CI systems that hold org credentials.
* **Do not print secrets.** There is no automatic redaction of command output.
  `echo "$NPM_TOKEN"`, debug dumps, or stack traces that include env can land
  in the Check Run log for anyone who can view the check.

## Related

* [Sandbox secrets](/sandboxes/secrets) — creating groups and file secrets
* [GitHub workflows](/ci/github-workflows) — job layout and path filters
* [Configuration](/ci/configuration) — full field reference
