{"record":{"id":"0ac33e49d8e42e85","repo":"nanocoai/nanoclaw","slug":"secret-shaped-env-key-on-container-role","errorCode":null,"errorMessage":"secret-shaped env '${key}' on ${container.role}","messagePattern":"secret-shaped env '(.+?)' on (.+?)","errorType":"validation","errorClass":"deniedByPolicy","httpStatus":null,"severity":"critical","filePath":"src/drivers/types.ts","lineNumber":480,"sourceCode":"        // as one escapes the read-only rule on the code the agent executes.\n        // Neither is exotic: both are a single word in a mount literal.\n        throw deniedByPolicy(`mount ${mount.hostPath} must be classed ${required}, not ${mount.class}`);\n      }\n      if (mount.class === 'install-surface' && mount.mode !== 'ro') {\n        throw deniedByPolicy(`install-surface mount ${mount.hostPath} must be ro`);\n      }\n      if (mount.class === 'identity-material' && (mount.mode !== 'ro' || container.role === 'agent')) {\n        // The no-credentials invariant, as a checkable rule: identity materials\n        // are ro-only and never enter the agent container.\n        throw deniedByPolicy(`identity-material mount ${mount.hostPath} invalid on role ${container.role}`);\n      }\n      if (!mountAllowed(mount, spec, policy)) {\n        throw deniedByPolicy(`mount ${mount.hostPath} violates class ${mount.class} scope ${mount.groupScope}`);\n      }\n    }\n    for (const [key, value] of Object.entries(container.env)) {\n      if (isSecretShaped(key, value)) {\n        throw deniedByPolicy(`secret-shaped env '${key}' on ${container.role}`);\n      }\n    }\n    for (const [key, value] of Object.entries(container.contributedEnv ?? {})) {\n      // The sanctioned lane: credential-shaped NAMES are its purpose — a\n      // provider registering `ANTHROPIC_AUTH_TOKEN=placeholder` for the proxy\n      // to overwrite is the pattern working as intended, and the name check\n      // alone denies every such install. Credential VALUES have no sanctioned\n      // channel, from anyone: real material rides mounts by reference.\n      if (looksLikeCredential(value)) {\n        throw deniedByPolicy(`credential value in contributed env '${key}' on ${container.role}`);\n      }\n    }\n  }\n}\n\n/**\n * The canonical absolute form the mount rules require: rooted, and free of\n * empty, '.' and '..' segments — so the string a prefix rule judges is the","sourceCodeStart":462,"sourceCodeEnd":498,"githubUrl":"https://github.com/nanocoai/nanoclaw/blob/294ef2aee85218b23ad30eda9dfe10e590b54a8c/src/drivers/types.ts#L462-L498","documentation":"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.","triggerScenarios":"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().","commonSituations":"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.","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."],"exampleFix":"// before\nenv: { ANTHROPIC_API_KEY: 'sk-ant-api03-real-token' }\n\n// after\nmounts: [{ hostPath: mats + '/anthropic-key', containerPath: '/creds/anthropic-key', class: 'identity-material', mode: 'ro' }],\nenv: { ANTHROPIC_CREDENTIALS_FILE: '/creds/anthropic-key' }","handlingStrategy":"validation","validationCode":"const CRED_NAME = /(API_KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)/i;\nfor (const c of spec.containers)\n  for (const [k, v] of Object.entries(c.env))\n    if (looksLikeCredential(v) || (CRED_NAME.test(k) && !v.startsWith('/')))\n      throw new Error(`refusing to pass credential in env '${k}' — use a by-reference mount`);","typeGuard":"function envValueIsExempt(v: string): boolean {\n  return v.startsWith('/') || v === 'placeholder' || v === '';\n}","tryCatchPattern":null,"preventionTips":["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."],"tags":["secrets","env","security","policy"],"backgroundTag":"secret-in-environment-variable","analyzedSha":"294ef2aee85218b23ad30eda9dfe10e590b54a8c","analyzedAt":"2026-08-28T13:59:10.357Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}