nanocoai/nanoclaw · error · deniedByPolicy
mount ${mount.hostPath} violates class ${mount.class} scope
Error message
mount ${mount.hostPath} violates class ${mount.class} scope ${mount.groupScope} What it means
The mount fails the class-scope check in mountAllowed: its class plus groupScope is not permitted for this spec and policy. Classes constrain where a hostPath may live and which sessions/groups may use it (e.g. group-scoped surfaces must stay under that group's root), so a mount whose scope does not cover its actual path or spec is denied.
Source
Thrown at src/drivers/types.ts:475
// only as strong as whoever assigns the class, and two of the four
// classes carry safety properties that a demotion silently drops:
// `allowlisted-extra` is permitted unconditionally, so relabelling a
// session private key as one mounts it INTO THE AGENT — defeating the
// no-credentials invariant outright — and relabelling the runner source
// 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}`);
}
}
}View on GitHub (pinned to 294ef2aee8)
Solutions
- Check the mount's class and groupScope against the policy roots: point hostPath inside the root the class+scope allows (e.g. your own group's root for group-scoped classes).
- If the path is intentionally shared, use the class whose scope covers it (such as a policy-allowlisted path) rather than widening a group scope.
- After moving group directories or changing policy roots, regenerate the spec so scopes and paths agree.
Example fix
// before
{ hostPath: '/groups/other-group/workspace', containerPath: '/workspace', class: 'group-surface', groupScope: 'my-group', mode: 'rw' }
// after
{ hostPath: '/groups/my-group/workspace', containerPath: '/workspace', class: 'group-surface', groupScope: 'my-group', mode: 'rw' } Defensive patterns
Strategy: validation
Validate before calling
// Before prepare(), assert each mount's hostPath sits under a root its class+scope permits:
for (const c of spec.containers)
for (const m of c.mounts)
if (!allowedRootsFor(m.class, m.groupScope, policy).some(r => m.hostPath === r || m.hostPath.startsWith(r + '/')))
throw new Error(`mount outside class scope: ${m.hostPath}`); Type guard
function underRoot(p: string, root: string): boolean {
return p === root || p.startsWith(root.endsWith('/') ? root : root + '/');
} Prevention
- Derive hostPaths from the group/policy roots at build time instead of hardcoding.
- After moving directories or changing policy roots, regenerate specs rather than reusing persisted ones.
- Keep a unit test asserting every shipped spec passes validateSpec against the current policy.
When it happens
Trigger: A mount declared with a groupScope that does not match the session's group, or a hostPath outside the root its class+scope permits — e.g. an 'allowlisted-extra' path that is not on the allowlist, or a group-scoped mount pointing at another group's directory. Emitted by validateSpec via mountAllowed(mount, spec, policy).
Common situations: Renaming/moving group folders so paths no longer sit under the declared group root; cross-group mounts attempted by composing specs by hand; policy root changes after an upgrade; stale groupScope values in persisted specs.
Related errors
- 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 ${
- mount ${mount.hostPath} must be a canonical absolute path (n
- secret-shaped env '${key}' on ${container.role}
AI-assisted analysis of nanocoai/nanoclaw@294ef2aee8 (2026-08-28).
Data as JSON: /api/errors/297f9bca16e216be.
Report an issue: GitHub.