Security findings 38
Code execution
criticalConfirmedhigh confidenceThe agent-runner's applyPreTaskScripts executes arbitrary shell code taken from any task message's content.script field by writing it to /tmp and spawning bash with the runner's full process.env before each provider query, so a planted task row runs arbitrary code with the runner's environment.
const child = spawn('bash', [scriptPath], { detached: true, env: process.env, stdio: ['ignore', 'pipe', 'pipe'] });vector:code
Counter-argument considered
The poll loop live-imports and calls applyPreTaskScripts on every batch, and runScript writes the task message's content.script to /tmp and spawns bash with the runner's process.env; no declared claim (description covers only the Atomic Chat MCP server) covers message-triggered shell execution, and no carve-out or gating of task rows appears in the shown units.
Code execution
highContextual risklow confidenceSKILL.md's pre-flight instructs installing the Atomic Chat desktop app from the unpinned GitHub 'latest release' dmg whenever the local API reachability check fails — a conditional prerequisite installer fetch on the host.
1. Install Atomic Chat from the [latest release](https://github.com/AtomicBot-ai/Atomic-Chat/releases) (macOS only for now — `atomic-chat.dmg`).
trigger:conditionalvector:instruction
Counter-argument considered
The same instruction viewed as EXEC: a conditional prerequisite fetch/install of an unpinned vendor dmg on the host is a concrete agent-directed action matching no carve-out (not pinned, not a fenced installer command), and the open declaration (D12) does not clear it.
Credential access
highUnverifiedhigh confidenceThe skill instructs the agent to patch the host's composeSessionSpec in src/container-runner.ts so the contributedEnv lane spreads atomicChatEnv() — which copies the host's ATOMIC_CHAT_API_KEY from process.env into container env — explicitly choosing that lane because it is exempt from the credential-NAME check that would otherwise refuse the spawn.
because `ATOMIC_CHAT_API_KEY` is credential-NAMED and the composed lane's key-name check would refuse the spawn
vector:instructionvector:mixed
Persistence
highContextual riskmedium confidenceThe runner's self-modification MCP tools let the container agent submit an arbitrary command with args and env as a new MCP server entry for container.json and trigger package installs with image rebuild and container restart, with only a host-side approval step (asserted in comments, not shown in these units) between the tool call and execution at restart.
return { config: { command, args: commandArgs, env } };vector:code
Counter-argument considered
Confirmed: the runner registers add_mcp_server/install_packages, the handler accepts a command string with no content restriction plus args/env and writes the kind:'system' action row, and the served host container-config.ts re-validates only shape (name charset, URL rules, env keys — command content unchecked) before materializing container.json at spawn; the approval enforcement lives in src/delivery.ts, served NOT CONSUMED and unshown, so the finding's characterization (approval asserted only in comments) remains accurate and nothing shown neutralizes it.
Code execution
highContextual risklow confidenceThe container's add_mcp_server MCP tool lets the agent submit wiring of an arbitrary local command with attacker-chosen args and env (or an HTTP URL) into its per-agent runtime config, which executes once an admin approves and the container restarts.
Provide either the local `command` + optional `args`/`env`, or its remote Streamable HTTP `url`
vector:code
Counter-argument considered
The tool is registered into the live MCP server and its handler accepts an arbitrary local command with agent-chosen args/env (only name and env-key charsets are validated) into the system-action payload the host applies; admin approval gates but does not neutralize the capability, and the skill's declared tools (atomic_chat_*) do not cover a self-service command installer.
Tool poisoning
highContextual riskmedium confidenceThe container MCP server advertises a tools list computed at runtime from a mutable registry, and any imported feature module can extend a registered tool (adding schema properties, appending description text, and copying agent call arguments verbatim into outbound system-action payloads), so the tool metadata the client receives is not fixed by the reviewed base source.
tools: allTools.map((t) => t.tool),
vector:codevector:metadata
Counter-argument considered
The client-visible tools list is computed at runtime from a mutable registry fed by side-effect module imports (including ../modules/index.js, not shown) and mutable via extendTool's description suffixes and schema merges, so the metadata the agent reads is not fixed by the reviewed base source; the terse '(not static)' declaration does not disclose this extension seam, and no evidence contradicts the finding.
Tool poisoning
highContextual riskmedium confidenceThe extendTool seam lets any module imported into the agent-runner additively extend a registered base tool — merging extra inputSchema properties, appending description text that conditions the agent, and copying declared argument keys verbatim into the base tool's outbound system-action payload (the mailbox rows behind install_packages/add_mcp_server) — without editing the base tool's source; constraints are that only declared passthroughKeys present in the call args are copied, handler-written keys are never overwritten, and schema collisions throw.
entries[key] = args[key];
vector:code
Counter-argument considered
extendTool is an exported seam that merges schema properties, appends agent-facing description text, and copies declared call-arg keys verbatim into the base handler's system-action payload via withOutboundPassthrough — undeclared anywhere in the skill's claims, with a concrete chain into the writeMessageOut mailbox rows; the constraints the finding itself lists do not neutralize it.
Persistence
highContextual risklow confidencewriteMemorySessionHook rewrites the Claude settings.json hooks.SessionStart array to run the command `bun /app/src/memory/hook.ts` (timeout 10) at every session start, a persistent agent control-plane hook write.
command: 'bun /app/src/memory/hook.ts',
vector:code
Counter-argument considered
The constant command is wired into the live Claude contract's lifecycle and written into Claude Code settings.json as a SessionStart command hook that runs at every session start; it persists (its dedup of legacy entries is hygiene, not cleanup of itself) and nothing in the skill's declaration covers it.
Data exfiltration
highContextual riskmedium confidenceThe agent-runner poll loop handles a /upload-trace chat command by calling uploadTrace(config.providerName) and logs that it uploads the session trace to Hugging Face, sending session/conversation data off-host to a third-party service.
log('Uploading session trace to Hugging Face');vector:code
Counter-argument considered
Best case against it: the upload goes to the authenticated user's own private dataset via an explicit admin-gated /upload-trace command, which lowers severity but does not neutralize it — the code still sends the full session transcript off-host to huggingface.co, and this artifact's declared scope (an Atomic Chat MCP server, with no declared claims at all) covers nothing about trace uploads, so no carve-out or declaration refutes the finding.
Data exfiltration
highContextual riskmedium confidenceThe poll loop's /upload-trace command reads the agent's newest Claude session transcript from ~/.claude/projects via readProviderTrace/newestClaudeTranscript and uploads the conversation trace to Hugging Face, sending conversation content off-host to a third-party service.
log('Uploading session trace to Hugging Face');trigger:conditionalvector:code
Counter-argument considered
Against it: the destination is the user's own private {hf_user}/nanoclaw-traces dataset behind an explicit command, which mitigates severity only; the served upload-trace.ts confirms the transcript read from ~/.claude/projects is base64-embedded and committed to huggingface.co, the ~/.claude/projects read is flagged as undeclared sensitive residue, and newestClaudeTranscript takes the newest .jsonl across all projects (broader than 'this session'), so nothing refutes the finding.
Code execution
highContextual risklow confidenceThe poll loop dynamically imports ./scheduling/task-script.js and executes pre-task scripts attached to incoming task rows before each provider call, running stored script content whose provenance and execution method are not shown in the cited units.
const { applyPreTaskScripts } = await import('./scheduling/task-script.js');trigger:conditionalvector:code
Counter-argument considered
Best case against it: the script runner is a deliberate scheduling feature with timeouts, process-group cleanup, and JSON output gating, but the served task-script.ts confirms the execution sink — task-row content.script is written to /tmp and run with bash, detached, inheriting process.env — and the script content originates from incoming message rows with no declared claim covering message-borne script execution, so the finding stands at reduced severity.
Persistence
highContextual riskmedium confidencewriteMemorySessionHook writes a hooks.SessionStart entry into <CLAUDE_CONFIG_DIR or ~/.claude>/settings.json that makes Claude Code execute 'bun /app/src/memory/hook.ts' (timeout 10, matcher startup|clear|compact) at every session start, and that hook target file is not part of any consumed agent/plugin/MCP content, so what runs at session start is unreviewable at this pin.
const filePath = path.join(claudeConfigDirectory(), 'settings.json');
vector:code
Counter-argument considered
Best case against it: the write is defensive (validates JSON, preserves unrelated hooks, removes only its own legacy entries), suggesting a legitimate memory shim — but the carve-out for validated hook shims requires a validated payload, and the hook's target bun /app/src/memory/hook.ts is NOT CONSUMED, i.e. outside every consumed agent/plugin/MCP content, while the settings.json SessionStart write itself is confirmed, wired through the provider lifecycle, and absent from all declared claims, so the persistence finding stands.
Credential access
highContextual risklow confidenceResidue R1 (unexplained, not concealed): the runner's provider-contract resolver defaults its environment parameter to the entire process.env and hands it to every provider capability resolver, an undeclared all-environment access with no off-host sink shown, which the skill's declared purpose of adding an Atomic Chat local-model MCP server does not cover.
environment: NodeJS.ProcessEnv = process.env,
vector:code
Counter-argument considered
The resolver literally defaults its environment parameter to the entire process.env and forwards it to every capability resolver, matching residue R1 and the detector hit; the claim is accurate and self-scoped (it asserts no off-host sink), and no carve-out or contradicting evidence applies.
Credential access
highContextual risklow confidenceProvider-contract realization hands the container's entire process environment (process.env) to provider capability resolution — undeclared residue (R2) beyond the declared purpose; no off-host sink for environment values is shown in this batch.
resolveCapability(memoryCapability, hook, process.env)
vector:code
Counter-argument considered
The call hands the whole process.env into the capability-resolution seam (residue R2) exactly as claimed; the fact that the Claude memory capability is a constant limits current flow but does not make the claim false, and no carve-out or mitigation evidence refutes it.
Credential access
highContextual risklow confidencenewestClaudeTranscript enumerates every project directory under ~/.claude/projects to return the newest SDK transcript path — undeclared residue (R3) beyond the skill's stated purpose; comments scope it to the /upload-trace diagnostic and this batch shows no network send, so the read's downstream destination remains unestablished.
const projects = path.join(os.homedir(), '.claude', 'projects');
vector:code
Counter-argument considered
newestClaudeTranscript enumerates every project directory under ~/.claude/projects and is contract-wired as readTrace for /upload-trace, matching residue R3 and the detector hit; the claim is accurate and self-scoped about the unestablished destination, so no gate fails and nothing refutes it.
Credential access
highContextual riskmedium confidenceUndeclared credential-scope widening: when Hugging Face sign-in fails, the setup message rewrites the credential gateway's pre-scoped secret URL by stripping the endpoint 'path' parameter, so the stored HF token will be authorized for all of huggingface.co rather than only the whoami endpoint.
setupUrl = e.secret_url.replace(/([?&]path=)[^&]*/, '$1');
vector:code
Counter-argument considered
The 401 setup path genuinely rewrites the gateway's pre-scoped secret_url by blanking the path parameter and the inline comment states the intent that the secret match all of huggingface.co; this credential-scope widening is covered by no declared claim and is not a listed carve-out.
Credential access
highContextual riskhigh confidencenotSignedInMessage strips the path-scoping parameter from the credential gateway's HF secret_url so the token the user is walked through creating grants the agent access to all huggingface.co endpoints rather than only /api/whoami-v2, deliberately widening credential scope so transcript uploads work.
setupUrl = e.secret_url.replace(/([?&]path=)[^&]*/, '$1');
vector:code
Counter-argument considered
notSignedInMessage strips the whoami-only path scoping from the gateway's secret_url and hands the widened URL to the user, and the code comment confirms this is deliberate so the token covers the upload endpoints; nothing declared covers this credential handling and no carve-out applies.
Data exfiltration
highContextual riskhigh confidenceThe /upload-trace path reads the active provider's session transcript file and base64-commits it to the user's {username}/nanoclaw-traces dataset via the Hugging Face API at huggingface.co, sending conversation content off-host.
`https://huggingface.co/api/datasets/${repo}/commit/main`trigger:conditionalvector:code
Counter-argument considered
uploadTrace reads the active provider's transcript file and base64-commits it to huggingface.co; although the trigger is an admin-gated /upload-trace command aimed at the user's own private dataset, off-host transfer of conversation content is not a carve-out and no declared claim of this skill covers it.
Credential access
highContextual riskmedium confidenceThe container spawn path collects the entire host environment (process.env, which can hold API keys and tokens) into a context object handed to provider adapter functions, so any credential in the host env reaches provider code whose downstream sink is not shown in these units.
hostEnv: process.env,
vector:code
Counter-argument considered
The served units show exactly what the finding claims: the spawn path puts the entire process.env into a context object and passes that context to the provider adapter (await fn(context)); wholesale env collection is not covered by the env-API-key-for-the-wrapped-service carve-out, no declared claim or mitigation covers it, and the DET lead flags env '*' as undeclared. The requested provider sink (src/providers) was not served, but the finding's claim is bounded at 'reaches provider code', which the shown code confirms, so it stands as the contextual_risk it was filed as and cannot be refuted.
Code execution
highUnverifiedlow confidencebuildAgentGroupImage interpolates DB-stored apt/npm package names (set via the referenced install_packages tool) unescaped into Dockerfile RUN lines and executes docker build through a shell starting from USER root, so unvalidated package strings could execute arbitrary commands at build time.
apt-get install -y ${aptPackages.join(' ')}vector:code
Credential access
highContextual risklow confidenceUndeclared residue R6-R8: the artifact's host driver code (src/drivers/index.ts) reads environment configuration (NANOCLAW_RUNTIME_DRIVER and NANOCLAW_SESSION_MATERIAL_ROOT from process.env with .env fallback) that the declared purpose of adding an Atomic Chat MCP server does not declare; the access is plainly visible, scoped to the driver's own two settings keys, and not concealed, so it is adjudicated unexplained rather than concealed.
export function readSetting(key: (typeof SETTINGS)[number], env: NodeJS.ProcessEnv = process.env): string {vector:code
Counter-argument considered
The env reads are real, sit on the executed driver-selection path, and are covered by no declared claim, but they are scoped to the module's two self-owned settings keys exactly as the claim states, so the low-confidence 'unexplained, not concealed' adjudication holds.
Credential access
highContextual risklow confidenceUndeclared residue (R9, adjudicated unexplained): the gateway-provider selection helper defaults its parameter to the whole process environment and also reads the local .env, consuming NANOCLAW_GATEWAY_PROVIDER — an environment-read capability the declared Atomic Chat MCP-server purpose does not declare, though only this single config variable is consumed and no sink is shown.
env: NodeJS.ProcessEnv = process.env
vector:code
Counter-argument considered
The helper does default to process.env with .env fallback and its result selects the orchestrated gateway provider, an undeclared capability relative to the Atomic Chat description; only the single NANOCLAW_GATEWAY_PROVIDER key is consumed, matching the claim's scoping.
Persistence
highContextual risklow confidenceThe host's claudeSettingsTransformer writes a PreCompact command hook running `bun /app/src/compact-instructions.ts` into the Claude settings.json consumed by the container agent, and the behavior of that hook script is not shown in this artifact.
preCompact.push({ hooks: [{ type: 'command', command: PRE_COMPACT_COMMAND }] });vector:code
Counter-argument considered
Refutation attempts fail: the hook payload's NOT CONSUMED status is never exculpatory, no declared claim (declared claims: none; description covers only the Atomic Chat MCP server) covers seeding PreCompact hooks, and the defensive AnchoredDir handling protects the settings file but does not validate or neutralize the hook. The transformer verbatim re-adds a persistent command hook into the container agent's settings.json while the script it runs is outside consumed content, so the claim stands.
Persistence
highContextual riskmedium confidenceNot covered by the declared purpose of adding an Atomic Chat MCP server skill, the host module migrate-claude-memory-settings.ts writes and rewrites the container agent's Claude settings.json, injecting a PreCompact hook whose command 'bun /app/src/compact-instructions.ts' executes a script whose content is not shown in this artifact's shown units.
dir.writeNewFile(CLAUDE_SETTINGS_FILE, Buffer.from(CLAUDE_DEFAULT_SETTINGS));
vector:code
Counter-argument considered
No cited evidence shows a carve-out or declaration: the module both writes settings.json from CLAUDE_DEFAULT_SETTINGS (which embeds the PreCompact hook) and rewrites it via the transformer, the container agent consumes that Claude state directory per group-init, and the declared purpose mentions only adding an Atomic Chat MCP server. The payload's absence from consumed content is confirmed and never makes the run instruction harmless.
Code execution
highContextual riskmedium confidenceThe host seeds and forcibly re-adds a PreCompact hook in Claude's settings.json that runs `bun /app/src/compact-instructions.ts` on every compaction, while that script is excluded from all consumed agent/client content, so unreviewed code auto-executes at compaction time.
const PRE_COMPACT_COMMAND = 'bun /app/src/compact-instructions.ts';
trigger:conditionalvector:code
Counter-argument considered
The claim is accurate on its face: the constant defines the compaction-time command, the transformer forcibly re-adds it whenever missing, and the script it executes is confirmed outside all consumed agent/client content, which is a reviewability gap, not an exculpation. No declaration, carve-out, or mitigation covering auto-execution of this unshown payload appears in the cited evidence.
Persistence
highContextual riskmedium confidenceThe host seeds and reconciles the container agent's settings.json with a persistent PreCompact command hook that executes 'bun /app/src/compact-instructions.ts', a repository file outside the content an agent/MCP client consumes, so a control-plane hook is installed whose payload cannot be reviewed here.
const PRE_COMPACT_COMMAND = 'bun /app/src/compact-instructions.ts';
vector:code
Counter-argument considered
The persistence claim survives: the host writes and reconciles the agent's settings.json with a PreCompact command hook whose payload is repository code outside consumable content, and nothing cited (defensive AnchoredDir handling, preservation of unrelated hooks, absence of the payload file) neutralizes the installed control-plane hook. The declared capability covers only the Atomic Chat MCP server, so the behavior is undeclared.
Persistence
highContextual risklow confidenceThe host composes and writes each group's CLAUDE.md project document on every container start, embedding stored agent-influenced text (persona, MCP server instructions) verbatim into the instruction file that becomes the containerized agent's system prompt, an agent-authored-to-future-agent-instruction channel that is plausibly the disclosed core mechanism of this platform.
stored, agent-influenced text copied verbatim into the system prompt
vector:code
Counter-argument considered
The host rewrites each group's instruction document on every spawn from stored agent-influenced sources and persists it via writeAtomic, undeclared by the target's description; the hedged 'plausibly the disclosed core mechanism' framing matches the visible design.
Persistence
highUnverifiedhigh confidenceAdjudicating residue R18 as unexplained (not concealed): the host runtime resolves the canonical instruction template at container/CLAUDE.md and composes it into a CLAUDE.md file written into every agent group's directory on each spawn (composeGroupProjectDoc -> writeAtomic), an agent instruction/config write that the artifact's declared purpose (adding an Atomic Chat MCP server) does not declare.
export const BASE_INSTRUCTIONS_PATH = path.join('container', 'CLAUDE.md');vector:code
Persistence
highContextual riskmedium confidenceAdjudicating residue R19 as unexplained (not concealed): the Claude provider host contract declares '/workspace/agent/CLAUDE.md' as the container mount path for the composed project document, the container-facing half of the same undeclared agent-instruction surface, uncovered by the declared purpose (the extracted file_read is verified to be this literal path declaration rather than a read operation).
containerPath: '/workspace/agent/CLAUDE.md',
vector:code
Counter-argument considered
The contract literally declares the composed document's container mount at /workspace/agent/CLAUDE.md and buildMounts consumes it as the undeclared container-facing half of the instruction surface; the claim correctly notes the detector's file_read label is a path declaration, not a read operation.
Credential access
highContextual riskmedium confidencerealizeProviderSpawnSurfaces copies the legacy provider adapter's returned env — produced from a context that includes hostEnv: process.env — verbatim into the provider contribution that composes the container's contributed environment, so host environment values can reach container env with no credential filtering visible in this file.
const contribution = overlay.env ? { env: overlay.env } : {};vector:code
Counter-argument considered
The realization path verifiably builds its context with hostEnv: process.env, hands it to the legacy adapter, and copies overlay.env unfiltered into the spawn contribution, so the undeclared host-env-to-container channel is as claimed; any adapter-side filtering lives outside the shown units and cannot refute the channel.
Supply chain
mediumContextual risklow confidenceThe skill instructs installing the third-party 'Atomic Chat' desktop app from an unpinned GitHub latest-release URL when its localhost API pre-flight fails, which would execute unversioned vendor code on the host.
Install Atomic Chat from the [latest release](https://github.com/AtomicBot-ai/Atomic-Chat/releases)
trigger:conditionalvector:instruction
Counter-argument considered
The install step is openly declared (D12) and conditional, but declaration does not neutralize it: the carve-outs require pinned official-registry installs or fenced vendor installer docs, and this is an unpinned GitHub latest-release dmg instruction in plain numbered steps with a concrete agent-directed chain.
Supply chain
mediumContextual riskmedium confidenceUndeclared by the skill's stated purpose: the runner exposes an MCP tool with which the container agent can request installation of up to 20 unpinned apt/npm packages into its own image, auto-executed via image rebuild and container restart once admin approval is granted.
Install apt and/or npm packages into YOUR per-agent container image. Requires admin approval; fire-and-forget. On approval, the image is rebuilt and the container is restarted automatically.
vector:code
Counter-argument considered
install_packages lets the agent request up to 20 unpinned apt/npm package names (the regexes explicitly bar version specs) that auto-apply via image rebuild and container restart on approval — a supply-chain capability outside the declared Atomic Chat purpose and its declared tool list, so no gate fails.
Privilege escalation
mediumContextual risklow confidenceThe Claude provider's configuration states the SDK's own permission prompts are disabled so that only container isolation and a tool allowlist gate the agent, a safety-prompt-disabling capability whose compensating controls are asserted in comments but whose confinement is not established from this evidence.
Claude runs unrestricted inside the container: NanoClaw's container * isolation and the OneCLI allow-list are the security boundary, not the * SDK's own permission prompts.
vector:code
Counter-argument considered
The artifact's own configuration text states the SDK's permission prompts are not the security boundary, and the concrete bypass (bypassPermissions fed into every sdkQuery) confirms the described disabling behavior; the comment's assertions of container isolation and allowlist are unproven compensating claims, not evidence that neutralizes the finding.
Privilege escalation
mediumContextual riskhigh confidenceNanoClaw's Claude provider returns permissionMode 'bypassPermissions' and allowDangerouslySkipPermissions true unconditionally, so every agent session runs with the SDK's permission prompts disabled.
permissionMode: 'bypassPermissions',
vector:code
Counter-argument considered
resolveClaudeExecutionPolicy unconditionally returns bypassPermissions and allowDangerouslySkipPermissions, is baked into the contract as a constant, and is passed into every sdkQuery call — an undeclared disabling of the agent SDK's permission controls that the detector also flags at high severity; no carve-out or mitigation evidence applies.
Supply chain
mediumContextual riskmedium confidenceUndeclared residue: the host image-build path allowlists pnpm build scripts by appending entries to /root/.npmrc so group-specified npm packages can run postinstall scripts, enabling install-time code execution during image builds, an install surface the declared purpose (adding an MCP server for local models) does not declare; exact code and allowlist scope are beyond the decoded fragment.
/root/.npmrc (base image sets it up for agent-browser) so packages
vector:code
Counter-argument considered
The build path deliberately allowlists every group-specified npm package in /root/.npmrc so its postinstall scripts execute during image builds; the in-code explanation is disclosure, not a carve-out (the installs are unpinned registry packages), and nothing in the declared description covers this install surface.
Agent hijacking
mediumContextual riskmedium confidencecomposeGroupProjectDoc re-reads the group's agent-writable persona (instructions.prepend.md from the group dir mounted RW at /workspace/agent) and stored MCP-server instructions strings and copies them verbatim into the composed CLAUDE.md on every spawn, giving container-agent-authored text a persistent path into the agent's own instruction file.
stored, agent-influenced text copied verbatim into the system prompt.
vector:code
Counter-argument considered
The composer verifiably copies the group-writable persona and stored MCP-server instructions into the composed instruction document on every spawn, an undeclared agent-to-future-agent instruction channel; O_NOFOLLOW and structural re-validation narrow the path but do not neutralize content injection, so the finding stands.
Agent hijacking
mediumUnverifiedlow confidencereadGroupPersona applies no content sanitization (anchored O_NOFOLLOW open, regular-file check, trim only) and composeGroupProjectDoc inlines the agent-writable persona verbatim as the first non-droppable 'Persona' section of CLAUDE.md, so authored standing instructions reach the system prompt unfiltered.
if (persona) push('Persona', persona);vector:code
Agent hijacking
mediumContextual risklow confidenceThe host reads container/CLAUDE.md on the host and inlines it as the leading 'NanoClaw Runtime Contract' section of every group's composed CLAUDE.md on every spawn, delivering standing instructions from a file outside the content an agent/MCP client consumes into every agent's instruction context.
export const BASE_INSTRUCTIONS_PATH = path.join('container', 'CLAUDE.md');vector:code
Counter-argument considered
The host-owned template is read on the host and inlined as the leading NanoClaw Runtime Contract section of every composed document on every spawn, a real undeclared instruction path; the canon is core-owned, consistent with the low-confidence disposition rather than grounds for refutation.