---
title: Secrets
description: "Script-based secret management — the three value modes, resolution order, cross-platform script execution, which secrets reach the container, error messages, and recipes for 1Password, Keychain, pass and AWS"
canonical: https://giglabo.com/heretic/docs/heretic-cli/configuration/secrets
locale: en
---

# Secrets

> Markdown twin of https://giglabo.com/heretic/docs/heretic-cli/configuration/secrets
> Fetch this instead of the HTML page: same content, a fraction of the bytes.
> Site structure and the full page list for agents: https://giglabo.com/llms.txt

Script-based secret management — the three value modes, resolution order, cross-platform script execution, which secrets reach the container, error messages, and recipes for 1Password, Keychain, pass and AWS

Tokens never live in YAML. The `secrets:` map holds **references**, resolved fresh on every run.

## The three value modes

Detected automatically from the value:

| Mode | Recognised by | Example |
|------|---------------|---------|
| **Script** | first token ends in `.sh`, `.cmd`, `.ps1` or `.bat` | `~/.heretic/get-key.sh` |
| **Environment reference** | the whole value is `$VAR` or `${VAR}` | `$MY_HOST_TOKEN` |
| **Literal** | anything else | `sk-ant-api03-…` |

```yaml
secrets:
  # script — executed, trimmed stdout becomes the value
  ANTHROPIC_API_KEY: "~/.heretic/get-anthropic-key.sh"
  ZAI_API_KEY: "~/.heretic/get-secret.sh zai"     # arguments are supported

  # environment reference — read from your shell
  GH_TOKEN: "$GITHUB_TOKEN"

  # literal — used as-is
  CUSTOM_KEY: "my-api-key-12345"
```

## Resolution order

1. Secrets are resolved **first**, before any `${VAR}` interpolation.
2. Resolved values join the interpolation context, so `env` can reference them.
3. Interpolation runs over the whole merged configuration.
4. The runner assembles the container environment.

```yaml
secrets:
  ANTHROPIC_API_KEY: "~/.heretic/get-anthropic-key.sh"
env:
  ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY}"    # this is what reaches the container
```

> **Warning: A secret must be referenced in env to reach the container**
>
> With the `docker` runner only secrets whose name also appears in `env` are exported — the rest exist purely for interpolation. The `compose` and `custom` runners are looser and pass the whole resolved map into the service environment (the custom runner exports it into the `docker compose` process so `${VAR}` inside your compose file resolves).

## Script requirements

- print the value to **stdout** (trailing whitespace is trimmed)
- exit 0 — a non-zero exit aborts the run
- executable on Unix (`chmod +x`; `heretic-cli init` writes mode `0755`)
- 30-second timeout
- `~` is expanded; arguments are allowed and quoted: `~/.heretic/get-secret.sh zai`

| Extension | Unix | Windows |
|-----------|------|---------|
| `.sh` | default shell | `bash` (Git Bash or WSL) |
| `.cmd` / `.bat` | n/a | `cmd.exe` |
| `.ps1` | n/a | `powershell -ExecutionPolicy Bypass -File` |

The same rule governs the tokens in `~/.heretic/settings.yaml`: a value ending in one of those extensions is executed, anything else is used as a raw token.

## Failure modes

| Situation | Result |
|-----------|--------|
| script path does not exist | `Secret script not found: <path>` — run aborts |
| script exits non-zero | `Secret script failed (<path>): <stderr>` — run aborts |
| `$VAR` reference and the variable is unset | `Secret "X": environment variable "Y" is not set` — run aborts |
| script prints nothing | `Secret script returned empty value` (warning); the variable ends up empty and is then **stripped** from the container environment |

Secret resolution is fail-fast: it happens before Docker is touched, so a broken script never leaves a half-started container behind.

## Example scripts

**1Password CLI**

```bash
#!/bin/bash
op read 'op://Personal/Anthropic API/credential'
```

**macOS Keychain** (argument-driven, one script for many secrets)

```bash
#!/bin/bash
security find-generic-password -s "heretic-$1" -w
```

**GitHub CLI**

```bash
#!/bin/bash
gh auth token
```

**pass (GPG-encrypted store)**

```bash
#!/bin/bash
pass show "heretic/$1"
```

**AWS Secrets Manager**

```bash
#!/bin/bash
aws secretsmanager get-secret-value --secret-id "$1" --query SecretString --output text
```

**Windows (1Password CLI)**

```cmd
@echo off
op read "op://Personal/Anthropic API/credential"
```

## Inspecting resolved values

```bash
heretic-cli agents show claude --resolved            # sensitive values masked
heretic-cli agents show claude --resolved --reveal   # plaintext
heretic-cli local-validate claude                    # plaintext, whole resolved object
```

Masking covers any environment key whose lowercase name contains `key`, `token`, `secret` or `password`.

> **Warning: Never paste --reveal or local-validate output anywhere**
>
> Both print real credentials. In CI, redirect them to a file you control, or use `agents show --resolved` without `--reveal`.

## Practices that hold up

- Keep one script per secret store, parameterised by argument, instead of dozens of `echo` scripts.
- Store the *reference* in git if you must (`.heretic/cli/*.yaml` is gitignored by convention), never the value.
- Rotate by changing the secret store — no heretic configuration changes needed.
- Prefer `$VAR` references in CI, where the runner already injects secrets into the environment: `secrets: { CLAUDE_API_KEY: $ANTHROPIC_API_KEY }`.

## Next Steps

- [init](https://giglabo.com/heretic/docs/heretic-cli/commands/init) — the scripts the wizard generates
- [Agent Profiles](https://giglabo.com/heretic/docs/heretic-cli/configuration/agent-profiles) — where `secrets` sits in the schema
- [Environment Variables](https://giglabo.com/heretic/docs/heretic-cli/reference/environment-variables) — what ends up inside the container

## Related

- HTML version of this page: https://giglabo.com/heretic/docs/heretic-cli/configuration/secrets
- Site map for agents: https://giglabo.com/llms.txt
