-
Notifications
You must be signed in to change notification settings - Fork 0
secrets
harnessed works fully without any secrets backend. This guide is for operators who
want 1Password-backed secrets resolved into their stacks at launch — opt-in, env-only,
never baked into an image or committed file. For the why (threat model, design rationale),
see docs/harnessed-design.md §16.
With a .env.schema present, secrets resolve from 1Password via varlock
and reach the pod members as environment variables only — never written to a profile, a
container image layer, or a repo file. Absent the schema, harnessed is bit-for-bit today's
behavior: varlock is never invoked, no op call, no env mutation. The opt-in switch is a
single filesystem test.
# 1. Copy the shipped template into your XDG harnessed config dir.
mkdir -p ~/.config/harnessed
cp .env.schema.example ~/.config/harnessed/.env.schema
# 2. Edit the op:// refs to match your 1Password vault items.
# Example (Private vault, items named "Snyk" and "SocketDev", field "credential"):
# SNYK_TOKEN=op(op://Private/Snyk/credential)
# SOCKET_CLI_API_TOKEN=op(op://Private/SocketDev/credential)
$EDITOR ~/.config/harnessed/.env.schema
# 3. Launch any stack. Resolved secrets reach the pod as env only.
harnessed claude_timeThe .env.schema.example shipped at the repo root is the canonical template — copy it,
don't author from scratch. The @plugin(@varlock/1password-plugin@1.2.0) +
@initOp(allowAppAuth=true) decorators are required (they wire the op(op://…) resolver
and tell varlock to use the mounted agent socket for app-auth).
harnessed runs varlock on the host to resolve op:// refs. This is required, not a
preference: 1Password's desktop app authorizes the op CLI by calling application (your
terminal), so app-auth (@initOp(allowAppAuth=true)) works on the host but cannot work
from inside a container — there the desktop app has no host app to bind the grant to, and op
fails with "cannot connect to 1Password app" no matter which socket is mounted. (The
~/.1password/agent.sock mounted into every stack is the SSH agent, for git signing — not
the op app-auth transport.)
- The launcher detects
~/.config/harnessed/.env.schema(one[ -f ]test — inert when absent; with no schema,varlockis never invoked). It then layers per-project env on top (see "Per-project env" below) — the steps here describe the global schema; a project schema resolves identically, just rooted at the project directory. - It runs
varlock load --format envon the host, in the schema's directory. The first run prompts the 1Password desktop app to Authorize your terminal for CLI access — approve it once and the grant persists. - The resolved dotenv is captured into a mode-0600 temp
--env-fileunder$TMPDIR. - That
--env-fileis spread into the launched container(s) — both pod members and the sidecar (svc up) — so resolved secrets reach the container as env only, never a profile, image layer, or repo file (T-05-05). This is launch-time only —harnessed buildnever touches secrets or invokes varlock at all (see "Scanner tokens" below for the separate, explicit path to a credentialed supply-chain scan). - The temp file is unlinked after launch (T-05-06).
This needs varlock on the host (npm i -g varlock); op (already on most 1Password hosts) is
driven by varlock via app-auth. The "podman-only host" invariant still holds for the
no-secrets path — varlock is never touched without a schema. Hosts without host varlock
fall back to the headless path below.
Beyond the user-global ~/.config/harnessed/.env.schema, the launcher also discovers env from
the project directory you launch in, and layers it on top of the global schema. Both
sources reach the pod; on a key collision, the project value wins (podman applies multiple
--env-file flags last-wins, and the launcher orders them [global, project]).
Two per-project cases, checked in order:
| Project has… | What happens |
|---|---|
<project>/.env.schema |
Resolved via varlock load in the project dir — varlock cascades any .env / .env.local overlays on top of the schema, and op:// refs resolve exactly as the global schema does. Result is a mode-0600 temp --env-file, unlinked after launch. |
<project>/.env only (no schema) |
No varlock invocation, no resolution — the file is copied into a mode-0600 temp --env-file with surrounding quotes and any export prefix stripped (so KEY="v" reaches the container as v, not "v" — podman keeps quotes literal). Your own .env is never modified; the temp is unlinked after launch. |
Notes:
- A project
.env.schematakes precedence over a plain.envin the same project (varlock already cascades the.env), so only one project source is used. - The plain-
.envpath never runs varlock — a project with only a.envneeds neithervarlockon PATH nor any 1Password backend. Values are read literally (quotes stripped); there is no variable interpolation orop://resolution on this path. - With no global schema and no project env,
harnessedis bit-for-bit today's behavior.
For environments without the 1Password desktop app or without host varlock (CI, the
nightly re-scan timer, a headless server), set OP_SERVICE_ACCOUNT_TOKEN in the launcher env.
With a service-account token, resolution runs host-native in-process (HTTPS bearer auth — no
desktop app, no app-auth, no socket). harnessed forwards the token only when it is already set
— it never prompts and never echoes.
Caution (per CLAUDE.md "What NOT to Use"): a visible service-account token leaks into any process sharing the env. Scope it narrowly to the invocation — prefix it on the command line (
OP_SERVICE_ACCOUNT_TOKEN=… harnessed claude_time) or inject via your CI secret store. Do notexportit in your shell profile or~/.bashrc, and do not leave it in a long-lived shell session.
Sidecar services (hindsight, openbrain, …) can declare their own schemas at
~/.config/<service>/.env.schema — e.g. ~/.config/hindsight/.env.schema for the hindsight
sidecar. The schema syntax is identical; see the .env.schema.example header comment.
harnessed ships four supply-chain scanners. Two are credential-free, two are token-gated:
| Scanner | Token | During build
|
During rescan
|
|---|---|---|---|
| osv-scanner | — | ✅ | ✅ |
| pip-audit | — | ✅ | ✅ |
| snyk | SNYK_TOKEN |
❌ never | ✅ |
| socket | SOCKET_CLI_API_TOKEN |
❌ never | ✅ |
harnessed build never invokes varlock and never touches secrets, full stop. Building a stack
(and verifying every recipe's skills/commands/rules/MCP servers installed correctly) must always
succeed with zero credentials, so recipe verification never depends on 1Password being available or
authorized. _build_derived_image issues a plain podman build unconditionally.
So the build's own in-image scan layer prints, every time:
· snyk: skipped (no SNYK_TOKEN — credentialed scan runs in `harnessed rescan`)
· socket: skipped (no SOCKET_CLI_API_TOKEN — credentialed scan runs in `harnessed rescan`)
That is correct, expected behavior — not a degraded state to fix.
harnessed rescan resolves your tokens on the host from ~/.config/harnessed/.env.schema (via
varlock) — or a bare ~/.config/harnessed/.env — and injects them into a throwaway container from
the image as a mode-0600 --env-file, which is unlinked afterwards. varlock never runs in-container
(1Password app-auth binds the grant to the calling host application; see "How resolution works").
harnessed rescan harnessed-claude-mystack:latest # one image
harnessed rescan # every harnessed-labelled imageharnessed build runs this for you against the image it just built, right after a successful
build — so a normal build does get a full snyk + socket scan, just as a separate credentialed pass
rather than inside the image layer. --no-security-scans skips it. It is advisory: it reports
posture and never fails the build.
# ~/.config/harnessed/.env.schema
SNYK_TOKEN=op(op://Private/Snyk/credential)
SOCKET_CLI_API_TOKEN=op(op://Private/SocketDev/credential)- Snyk token: https://app.snyk.io/account/personal-access-tokens (Account settings → Personal Access Tokens → Generate).
-
Socket token: https://socket.dev/dashboard/settings/api-tokens — needs the
full-scans:createandpackages:listpermissions. -
SOCKET_SECURITY_API_KEY(the name Socket's GitHub Action uses, and what earlier versions of this guide told you to declare) is still accepted as an alias, butSOCKET_CLI_API_TOKENis canonical and wins when both are set. - You do not need
SOCKET_CLI_ORG_SLUG: the scan derives your org from the token. Set it only to pin a specific org when the token can see more than one.
Socket has no offline mode — it uploads a synthesized manifest of the installed tree, so each scanned
tree costs 1 Socket quota unit. Scans are created with --tmp, which keeps them off your org's
alerts page.
After launching with a schema present, confirm secrets reached the pod and did not leak:
# 1. The value reached the pod env (replace <instance> + <KEY> with your own):
podman exec <instance> env | grep SNYK_TOKEN
# expected: SNYK_TOKEN=<the resolved value>
# 2. The value is NOT in the committed profile:
grep -r SNYK_TOKEN profiles/ # expected: no matches
# 3. The value is NOT baked into the image:
podman history harnessed-base:latest | grep -i snyk # expected: no matches
# 4. The temp env-file is gone after launch:
ls /tmp/harnessed-env.* 2>/dev/null # expected: no matches (unlinked post-launch)If podman exec … env | grep <KEY> returns nothing but you expected it to, check that:
-
~/.config/harnessed/.env.schemaexists and has a non-@optionalentry for<KEY>. - The 1Password desktop app is running and unlocked (the agent socket must be live for
app-auth), OR
OP_SERVICE_ACCOUNT_TOKENis set in the launcher env. - The
op(op://Vault/Item/field)ref points at a real vault item.
- docs/harnessed-design.md §16 — the design rationale (the why).
- docs/harnessed-design.md §7 — supply-chain scanners + the warn-and-skip contract.
- .env.schema.example — the canonical schema template.
Start Here
Guides
- Recipe authoring
- Service authoring
- Stacks
- Extending stacks (proposed)
- Recipe catalog
- System prompt & rules (proposed)
- Secrets
- AWS SSO
- Pulumi (host login forwarding)
- Egress & exposing services
- Container filesystem
- Git hooks
- Troubleshooting
- Pin management (harnessed update)
Codebase Map
Planning & Roadmap
- open work: GitHub Issues
Research & Prompts
- research/ (home-folder requirements per harness, browse in-repo)
- prompts/ (reusable prompt templates, browse in-repo)