nanocoai/nanoclaw · error · specInvalid
duplicate containerPath ${mount.containerPath} on ${containe
Error message
duplicate containerPath ${mount.containerPath} on ${container.role} What it means
Two mounts on the same container target the same containerPath. Which source wins would then depend on mount ordering inside the runtime, an artifact the model refuses to rely on; composition is supposed to resolve collisions (contributed mounts win) so a spec reaching the driver has exactly one source per container path.
Source
Thrown at src/drivers/types.ts:448
for (const mount of container.mounts) {
if (!hostPathCanonical(mount.hostPath)) {
// Every class rule below is a prefix check against a trusted root, and
// a prefix check reads `materialsRoot/../outside` as inside — the
// runtime then normalizes it OUTSIDE the root it was judged against.
// Requiring the canonical absolute form makes the string these rules
// judge the same path the runtime mounts. (A relative source would not
// even be a bind: Docker reads it as a named volume.) Symlinks remain
// beyond a lexical check — that is what `admissionEnforced`
// realizations are for.
throw deniedByPolicy(
`mount ${mount.hostPath} must be a canonical absolute path (no '..', '.', '//', or trailing '/')`,
);
}
if (seenTargets.has(mount.containerPath)) {
// Two sources for one target would make the realized mount an ordering
// artifact. Composition resolves collisions (contributed mounts win),
// so a spec reaching a driver has exactly one source per target.
throw specInvalid(`duplicate containerPath ${mount.containerPath} on ${container.role}`);
}
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') {View on GitHub (pinned to 294ef2aee8)
Solutions
- Search the spec for the duplicated containerPath named in the message and remove or rename one of the mounts so each target has exactly one source.
- If merging contributed mounts, resolve collisions before validateSpec: keep the contributed mount and drop the base one (matching the composition rule).
- Dedupe by containerPath with a Map keyed on the target when building the spec programmatically.
Example fix
// before
mounts: [
{ hostPath: '/groups/a/workspace', containerPath: '/workspace', class: 'group-surface', mode: 'rw' },
{ hostPath: '/shared/stubs', containerPath: '/workspace', class: 'allowlisted-extra', mode: 'ro' },
]
// after
mounts: [
{ hostPath: '/groups/a/workspace', containerPath: '/workspace', class: 'group-surface', mode: 'rw' },
{ hostPath: '/shared/stubs', containerPath: '/stubs', class: 'allowlisted-extra', mode: 'ro' },
] Defensive patterns
Strategy: validation
Validate before calling
function dedupeMountTargets(spec: SessionSpec): void {
for (const c of spec.containers) {
const byTarget = new Map(c.mounts.map(m => [m.containerPath, m]));
if (byTarget.size !== c.mounts.length) {
// keep the contributed mount per composition rule, or throw explicitly
throw new Error(`duplicate containerPath on ${c.role}`);
}
}
} Type guard
function hasUniqueMountTargets(mounts: MountSpec[]): boolean {
return new Set(mounts.map(m => m.containerPath)).size === mounts.length;
} Prevention
- Dedupe merged mount lists by containerPath before validateSpec.
- When contributing mounts, drop the base mount with the same target instead of appending.
- Key mount collections by containerPath in spec builders so duplicates are structurally impossible.
When it happens
Trigger: A spec literally lists two mounts with the same containerPath on one container, e.g. both '/workspace' and '/plugins' mapping to '/opt/stuff', or a hand-merged spec where a base mount and an added mount both claim '/app'. Raised as kind 'spec-invalid' from validateSpec during prepare().
Common situations: Merging mount lists from multiple sources (base template + group config + provider contributions) without deduping by containerPath; refactoring a containerPath constant and forgetting one occurrence; adding a new mount that shadows an existing default like /workspace.
Related errors
- mount ${mount.hostPath} must be a canonical absolute path (n
- --stdin-json input is empty
- Container name collision: existing container is not this ses
- mount ${mount.hostPath} must be classed ${required}, not ${m
- install-surface mount ${mount.hostPath} must be ro
AI-assisted analysis of nanocoai/nanoclaw@294ef2aee8 (2026-08-28).
Data as JSON: /api/errors/424dd1d8d3b5ba7d.
Report an issue: GitHub.