Secrets
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-… |
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
- Secrets are resolved first, before any
${VAR}interpolation. - Resolved values join the interpolation context, so
envcan reference them. - Interpolation runs over the whole merged configuration.
- The runner assembles the container environment.
secrets:
ANTHROPIC_API_KEY: "~/.heretic/get-anthropic-key.sh"
env:
ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY}" # this is what reaches the container
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 initwrites mode0755) - 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
#!/bin/bash
op read 'op://Personal/Anthropic API/credential'
macOS Keychain (argument-driven, one script for many secrets)
#!/bin/bash
security find-generic-password -s "heretic-$1" -w
GitHub CLI
#!/bin/bash
gh auth token
pass (GPG-encrypted store)
#!/bin/bash
pass show "heretic/$1"
AWS Secrets Manager
#!/bin/bash
aws secretsmanager get-secret-value --secret-id "$1" --query SecretString --output text
Windows (1Password CLI)
@echo off
op read "op://Personal/Anthropic API/credential"
Inspecting resolved values
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.
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
echoscripts. - Store the reference in git if you must (
.heretic/cli/*.yamlis gitignored by convention), never the value. - Rotate by changing the secret store — no heretic configuration changes needed.
- Prefer
$VARreferences in CI, where the runner already injects secrets into the environment:secrets: { CLAUDE_API_KEY: $ANTHROPIC_API_KEY }.
Next Steps
- init — the scripts the wizard generates
- Agent Profiles — where
secretssits in the schema - Environment Variables — what ends up inside the container