nanocoai/nanoclaw · critical · deniedByPolicy
secret-shaped env '${key}' on ${container.role}
Error message
secret-shaped env '${key}' on ${container.role} What it means
An entry in container.env is 'secret-shaped': either a credential-named key (like API_KEY/TOKEN/SECRET patterns) carrying a non-exempt value, or a value that itself looks like a credential regardless of key name. The invariant is that no credential VALUE rides in the environment; material must pass by reference (a read-only mount plus an env var holding its path) or through the sanctioned contributedEnv/proxy lane.
Source
Thrown at src/drivers/types.ts:480
// as one escapes the read-only rule on the code the agent executes.
// Neither is exotic: both are a single word in a mount literal.
throw deniedByPolicy(`mount ${mount.hostPath} must be classed ${required}, not ${mount.class}`);
}
if (mount.class === 'install-surface' && mount.mode !== 'ro') {
throw deniedByPolicy(`install-surface mount ${mount.hostPath} must be ro`);
}
if (mount.class === 'identity-material' && (mount.mode !== 'ro' || container.role === 'agent')) {
// The no-credentials invariant, as a checkable rule: identity materials
// are ro-only and never enter the agent container.
throw deniedByPolicy(`identity-material mount ${mount.hostPath} invalid on role ${container.role}`);
}
if (!mountAllowed(mount, spec, policy)) {
throw deniedByPolicy(`mount ${mount.hostPath} violates class ${mount.class} scope ${mount.groupScope}`);
}
}
for (const [key, value] of Object.entries(container.env)) {
if (isSecretShaped(key, value)) {
throw deniedByPolicy(`secret-shaped env '${key}' on ${container.role}`);
}
}
for (const [key, value] of Object.entries(container.contributedEnv ?? {})) {
// The sanctioned lane: credential-shaped NAMES are its purpose — a
// provider registering `ANTHROPIC_AUTH_TOKEN=placeholder` for the proxy
// to overwrite is the pattern working as intended, and the name check
// alone denies every such install. Credential VALUES have no sanctioned
// channel, from anyone: real material rides mounts by reference.
if (looksLikeCredential(value)) {
throw deniedByPolicy(`credential value in contributed env '${key}' on ${container.role}`);
}
}
}
}
/**
* The canonical absolute form the mount rules require: rooted, and free of
* empty, '.' and '..' segments — so the string a prefix rule judges is theView on GitHub (pinned to 294ef2aee8)
Solutions
- Remove the credential value from env; instead mount the credential file read-only under its proper class and put the file PATH in the env var (paths are exempt).
- Or route through the proxy/contributedEnv lane: register a credential-named key with a placeholder the proxy overwrites at runtime.
- For tests, use obviously-fake placeholder values the exemptions accept, or better, the same by-reference pattern.
Example fix
// before
env: { ANTHROPIC_API_KEY: 'sk-ant-api03-real-token' }
// after
mounts: [{ hostPath: mats + '/anthropic-key', containerPath: '/creds/anthropic-key', class: 'identity-material', mode: 'ro' }],
env: { ANTHROPIC_CREDENTIALS_FILE: '/creds/anthropic-key' } Defensive patterns
Strategy: validation
Validate before calling
const CRED_NAME = /(API_KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)/i;
for (const c of spec.containers)
for (const [k, v] of Object.entries(c.env))
if (looksLikeCredential(v) || (CRED_NAME.test(k) && !v.startsWith('/')))
throw new Error(`refusing to pass credential in env '${k}' — use a by-reference mount`); Type guard
function envValueIsExempt(v: string): boolean {
return v.startsWith('/') || v === 'placeholder' || v === '';
} Prevention
- Pass credentials by reference: ro mount + env var holding the file path.
- Never copy .env/docker-compose credential entries into specs.
- Run a secrets scanner over generated specs in CI.
When it happens
Trigger: Setting spec env like ANTHROPIC_API_KEY='sk-ant-...' or any var whose value matches credential patterns (long base64/hex/bearer tokens). Thrown from validateSpec's loop over Object.entries(container.env) during prepare().
Common situations: Porting a docker-compose or .env setup straight into a spec; passing real tokens via env 'temporarily' in tests; renamed key attempts (GW_CRED, SESSION_BEARER) that the value-shape check still catches.
Related errors
- credential value in contributed env '${key}' on ${container.
- ${settingsFile} hooks.SessionStart must be an array
- mount ${mount.hostPath} must be classed ${required}, not ${m
- install-surface mount ${mount.hostPath} must be ro
- identity-material mount ${mount.hostPath} invalid on role ${
AI-assisted analysis of nanocoai/nanoclaw@294ef2aee8 (2026-08-28).
Data as JSON: /api/errors/0ac33e49d8e42e85.
Report an issue: GitHub.