nanocoai/nanoclaw · critical · deniedByPolicy
install-surface mount ${mount.hostPath} must be ro
Error message
install-surface mount ${mount.hostPath} must be ro What it means
A mount classified 'install-surface' (code the runtime installs/executes: runner source, plugins) is declared with mode 'rw'. Install surfaces must be read-only so the agent cannot modify the very code it runs — a writable install surface is self-modification by another name.
Source
Thrown at src/drivers/types.ts:467
}
seenTargets.add(mount.containerPath);
const required =
classRequiredByPath(mount.hostPath, policy) ??
(pluginsRoot && underRoot(mount.hostPath, pluginsRoot) ? 'install-surface' : null);
if (required && mount.class !== required) {
// Where a file lives decides what it IS, so the class is not the
// composer's to choose for these roots. Without this the taxonomy is
// 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 proxyView on GitHub (pinned to 294ef2aee8)
Solutions
- Change the mount's mode to 'ro' as the message states.
- If the container genuinely must write there, that location is not an install surface — mount a different (writable-class) target path for the writes and keep the install surface ro.
- Audit spec generators/templates so 'install-surface' entries always emit mode 'ro'.
Example fix
// before
{ hostPath: '/opt/nanoclaw/runner', containerPath: '/runner', class: 'install-surface', mode: 'rw' }
// after
{ hostPath: '/opt/nanoclaw/runner', containerPath: '/runner', class: 'install-surface', mode: 'ro' } Defensive patterns
Strategy: type-guard
Validate before calling
const bad = spec.containers.flatMap(c => c.mounts.filter(m => m.class === 'install-surface' && m.mode !== 'ro'));
if (bad.length) throw new Error(`install-surface mounts must be ro: ${bad.map(m => m.hostPath).join(', ')}`); Type guard
function isReadOnlyInstallSurface(m: MountSpec): boolean {
return m.class !== 'install-surface' || m.mode === 'ro';
} Prevention
- Make spec builders default mode to 'ro' and require an explicit opt-in for 'rw'.
- Code-review any 'rw' mount whose hostPath touches runner/plugin source.
- Keep writable scratch on a separate containerPath from install surfaces.
When it happens
Trigger: A spec mount with class 'install-surface' and mode: 'rw' (or any non-'ro' value) on any container. Commonly a typo or a copied mount literal from an rw workspace mount.
Common situations: Cloning a group-surface rw mount entry and changing hostPath to a plugins/surface root without flipping mode; debugging sessions where someone temporarily made everything writable and forgot to revert; automated spec generators that default mode to 'rw'.
Related errors
- mount ${mount.hostPath} must be classed ${required}, not ${m
- identity-material mount ${mount.hostPath} invalid on role ${
- mount ${mount.hostPath} violates class ${mount.class} scope
- 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/e8da9e0e98779d80.
Report an issue: GitHub.