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

# Secrets in CI

> Run the CLI in a pipeline without an interactive login.

The CLI is built to run unattended. Two environment variables replace `sink login`, and every destructive command refuses to guess when there is no terminal to ask.

## Credentials

```bash theme={null}
export SINK_API_URL=https://usesink.co
export SINK_API_KEY=sk-…
```

`SINK_API_KEY` beats anything in the keychain or the config file, so it works on a runner that has never seen `sink login`. Store it in your CI provider's own secret store — never in the repository.

Mint a dedicated key per pipeline under **API Keys** in the dashboard. Give it an expiry, and revoke it rather than rotating a shared one when a runner is retired.

<Note>
  `.sink.json` is committed, so the runner already knows which workspace, team, project and environment to use. Pass an environment name explicitly when a job targets a different one.
</Note>

## Fetching secrets for a build

```bash theme={null}
sink pull prod --force
```

`--force` is what makes this safe to re-run: without it the command refuses to overwrite an existing `.env` and, with no terminal attached, fails rather than prompting.

To avoid writing a file at all:

```bash theme={null}
sink pull prod --stdout >> "$GITHUB_ENV"
sink pull prod --format json --stdout | jq '.DATABASE_URL'
```

## Failing a build on drift

```bash theme={null}
sink diff prod --exit-code
```

Exits `1` when the local file and the environment disagree, `0` when they match. Useful as a guard on a repo that keeps a checked-in `.env.example`, or as a post-deploy assertion that what shipped is what Sink holds.

## Pushing from a pipeline

```bash theme={null}
sink push prod --yes
```

`--yes` is mandatory without a terminal — the command refuses to write otherwise. Add `--dry-run` first if you want the plan in the log before the run that applies it.

<Warning>
  `--prune` deletes remote secrets that are absent from the local file. In a pipeline, where the file may be incomplete for reasons that have nothing to do with intent, that is a bad combination. Prune from a workstation, after a `--dry-run`.
</Warning>

## GitHub Actions

```yaml .github/workflows/deploy.yml theme={null}
jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      SINK_API_URL: https://usesink.co
      SINK_API_KEY: ${{ secrets.SINK_API_KEY }}
    steps:
      - uses: actions/checkout@v4

      - name: Install Sink
        run: |
          curl -fsSL https://usesink.co/install.sh | sh
          echo "$HOME/.local/bin" >> "$GITHUB_PATH"

      - name: Fetch secrets
        run: sink pull prod --force

      - name: Deploy
        run: ./deploy.sh
```

<Tip>
  Add `.env` to `.gitignore` and to any build-context ignore file (`.dockerignore`) before wiring this up. `sink pull` writes the file `0600`, which protects it from other users on the runner — not from being copied into an image layer.
</Tip>

## Pinning a version

```bash theme={null}
sink pull prod --version 2 --force
```

Reads each secret at version 2. Remember that version numbers run **per secret**, so this is not an environment-wide snapshot — secrets with fewer versions come back at their latest, and the CLI says which ones. See [Core concepts](/concepts#versions).

## Rate limits

Secret reads and writes are budgeted per credential at 120 requests a minute and 2,000 an hour. The CLI batches its work — one call to list, one bulk create, one bulk update — so a normal pull or push costs a handful of requests regardless of how many keys are involved. A job that loops one secret at a time is what runs into the ceiling.

A `429` carries `Retry-After`; wait it out rather than retrying immediately.
