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 the

View on GitHub (pinned to 294ef2aee8)

Solutions

  1. 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).
  2. Or route through the proxy/contributedEnv lane: register a credential-named key with a placeholder the proxy overwrites at runtime.
  3. 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

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


AI-assisted analysis of nanocoai/nanoclaw@294ef2aee8 (2026-08-28). Data as JSON: /api/errors/0ac33e49d8e42e85. Report an issue: GitHub.