Announcing Beacon: Self-improving memory for coding agents
← All skills
Skill

add-atomic-chat-tool

skill:nanocoai/nanoclaw#add-atomic-chat-tool

Add Atomic Chat MCP server so the container agent can call local models served by the Atomic Chat desktop app via its OpenAI-compatible API.

Verdict
Malicious

Confirmed harmful behavior that reaches the agent or host.

Analyzed Oct 27, 2026, 22:54 UTC at acc2b1c

Dispute security verdict

If you believe this verdict is incorrect, submit a dispute and we'll review it.

38
findings
critical
highest severity
8
threat classes
1,343
files scanned
2,420/2,421
checks satisfied
0
package advisories

Why this verdict

  • malicious: finding skill:.claude/skills/add-atomic-chat-tool:container/agent-runner/src/scheduling/task-script.ts:26:EXEC passes reach/harm/chain at critical

Security findings 38

Code execution

criticalConfirmedhigh confidence

The 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 confidence

SKILL.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 confidence

The 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 confidence

The 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 confidence

The 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 confidence

The 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 confidence

The 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 confidence

writeMemorySessionHook 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 confidence

The 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 confidence

The 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 confidence

The 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 confidence

writeMemorySessionHook 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 confidence

Residue 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 confidence

Provider-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 confidence

newestClaudeTranscript 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 confidence

Undeclared 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 confidence

notSignedInMessage 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 confidence

The /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 confidence

The 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 confidence

buildAgentGroupImage 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 confidence

Undeclared 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 confidence

Undeclared 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 confidence

The 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 confidence

Not 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 confidence

The 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 confidence

The 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 confidence

The 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 confidence

Adjudicating 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 confidence

Adjudicating 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 confidence

realizeProviderSpawnSurfaces 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 confidence

The 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 confidence

Undeclared 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 confidence

The 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 confidence

NanoClaw'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 confidence

Undeclared 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 confidence

composeGroupProjectDoc 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 confidence

readGroupPersona 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 confidence

The 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.

Open questions 49

What the analysis could not settle from the code alone.

The runner's MCP server tools list folds as unresolved because array 'allTools' may be mutated in place at container/agent-runner/src/mcp-tools/server.ts:37 — which tools does the runner MCP server actually expose to the agent?
Tool poisoning
Which shipped modules call extendTool/withOutboundPassthrough to merge extra entries into system-action outbound payloads, and what data do those entries carry?
Tool poisoning
Which CA certificate does browser-trust.ts import into the agent browser's ~/.pki/nssdb trust store, and is it limited to the deployed gateway CA?
Persistence
The declared MCP tool metadata for atomic_chat_list_models and atomic_chat_generate (kind=metadata units) was not present in this batch, so tool names, descriptions, and parameter descriptions cannot be checked here for poisoning or context harvesting.
Tool poisoning
The owed unit is delivered as decoded content recovered from an encoded or hidden channel, but its decoded text is plain database-schema documentation (messaging groups, users, role grants, approvals) for a host system unrelated to the declared Atomic Chat MCP skill; from what channel was this content recovered, and does the concealment serve any purpose beyond the declared task?
The target is declared as the skill 'add-atomic-chat-tool' (adding an Atomic Chat MCP server), but every unit in this batch is NanoClaw container agent-runner infrastructure (mailbox SQLite, message formatting, heartbeat, MCP config consumption) and none mention Atomic Chat; whether the skill's additions are limited to the declared MCP server entry cannot be settled from this batch.
Where are the target's declared atomic_chat_list_models / atomic_chat_generate tools implemented: does the add-atomic-chat-tool skill register them into this MCP server via registerTools/extendTool or ship a separate server, and what tool metadata (names, descriptions, schemas) do they carry?
Tool poisoning
No unit in this batch references Atomic Chat or its MCP server registration, and the provider-contract barrel shows only the Claude import, so whether the declared atomic_chat_list_models/atomic_chat_generate tools are actually wired by this skill cannot be settled from the units shown.
Where does /upload-trace send the trace returned by readProviderTrace/newestClaudeTranscript — does transcript content from ~/.claude/projects leave the host, and to what destination?
Data exfiltration
Do the skill's shipped files include these container/agent-runner provider-infrastructure files (making the bypassPermissions execution policy and pre-task script runner the skill's additions), or are they base runner code the skill merely integrates with?
The owed units are host-side container-orchestration sources (src/config.ts, src/container-config.ts, src/container-runner.ts) whose breadth far exceeds the skill's declared purpose of adding an Atomic Chat MCP server; whether these files ship inside the skill artifact itself or belong to a separate host component of the same repository is not established from this batch.
Which host-facing tool or route exposes updateContainerConfigJson('packages_npm'/'packages_apt') or buildAgentGroupImage to agents — i.e., can an agent trigger an arbitrary-package image build with build scripts enabled on the host?
Supply chain
The bundle ships an entire multi-platform messaging agent host under src/ (SQLite migrations, session and approval stores, Slack/Discord/Telegram/WhatsApp/iMessage/Matrix/email adapter references, container orchestration and driver selection) while the declared purpose covers only adding an Atomic Chat MCP server with two tools - is this host code part of the skill artifact's declared scope or is it the installation-target repository the skill operates on, and if it ships with the skill, which d⟦… 21 more chars …⟧
Residue R9 (env '*') was adjudicated unexplained at low confidence: does the add-atomic-chat-tool integration register through the gateway-provider barrel (making NANOCLAW_GATEWAY_PROVIDER selection necessary plumbing for the declared purpose), or is this trunk environment access unrelated to the declared MCP-server purpose?
Credential access
All owed units are host platform sources under src/ (mount security, typing, provider contracts) while the target declares only an Atomic Chat MCP skill; whether these runtime files are part of the shipped artifact surface or unrelated repository content cannot be settled from this batch.
Does the contributedEnv lane's credential-shaped value check reject a real ATOMIC_CHAT_API_KEY secret (e.g. an sk-... value), or does the credential-named key pass through to the container env unchanged?
Credential access
The declared skill adds Atomic Chat MCP tools (atomic_chat_list_models, atomic_chat_generate), but no unit shown registers or references them; which code implements the MCP server this skill actually adds, and does adding it expose the agent-runner toolset (send_file, install_packages, add_mcp_server) to the skill user?
Tool poisoning
The origin and write-access control for task messages carrying a content.script field is not shown; if chat participants or remote content can create such tasks, runScript converts them into arbitrary shell execution inside the container.
Code execution
Does this artifact ship or reference the 'remote-backend skill' said to replace src/db/compose.ts, and if so, does it redirect central-DB access (including session and group data) to a remote host?
Supply chain
Are packages_apt/packages_npm values validated as package names (no shell metacharacters) anywhere before they reach buildAgentGroupImage's Dockerfile interpolation?
Code execution
The served units are core host-framework sources (src/db/*, src/drivers/*) that never mention Atomic Chat; whether the add-atomic-chat-tool skill ships, imports, or modifies these files is not established by this pass, so no source-to-sink chain involving the skill itself can be confirmed from the shown content.
Detector lead DET:3f1e60c3868e150e flagged 'silently run' as HIJACK secrecy; the phrase in SRC:99fbc790ed1beb39 describes preventing a host from silently running the wrong runtime on a config typo, not an instruction to act without the user's knowledge — treated as refuted, recorded here for the refuter.
Agent hijacking
The target is declared as a skill adding an Atomic Chat MCP server, but every unit in this batch is host-platform code (agent-to-agent messaging DBs, mount security, typing indicator, provider contracts) and none implements or references the Atomic Chat MCP server; where is the skill's own content and why does this batch consist of unrelated host modules?
The src/providers/index.ts barrel comment states that skills add a new provider by appending one import line to that file, which would execute skill-supplied host code at import time; which components perform such appends and what do those imported modules execute?
Code execution
[file request container/agent-runner/src/browser-trust.ts] Which certificate does trustGatewayCaForChromium install into Chromium's NSS trust store, and could it enable gateway interception of browser traffic?
Privilege escalation
[file request container/agent-runner/src/memory/hook.ts] What does the memory session-start hook (bun /app/src/memory/hook.ts) read, write, or execute — does it do anything beyond rendering the shared memory tree into the session-start context?
Persistence
[file request container/agent-runner/src/mailbox/index.ts] Can chat-platform inbound messages be written into the mailbox as kind='task' rows whose content includes a script field (executed by applyPreTaskScripts with bash), or is task-row creation restricted to host operator scheduling?
Code execution
[file request src/mount-allowlist.ts] Does this file define validateAdditionalMounts, and what hostPath/containerPath forms does it accept for containerConfig.additionalMounts — can additional mounts expose host paths beyond the group subtree?
Persistence
[file request container/agent-runner/src/modules/index.ts] Which modules does this barrel load at import time, and do any of them call registerTools or extendTool (e.g., extending install_packages/add_mcp_server)?
Tool poisoning
[file request src/db/container-configs.ts] Where does the host enforce admin approval (validateAddMcpServer on original submission and approval replay) before an add_mcp_server submission is written to the container_configs row that materializes container.json?
Persistence
[file request src/compact-instructions.ts] What does the PreCompact hook command `bun /app/src/compact-instructions.ts` execute when the container agent compacts?
Code execution
[file request src/container-runner.ts] Does the contributedEnv lane in composeSessionSpec actually refuse credential-shaped VALUES, and does it exempt credential-named keys such as ATOMIC_CHAT_API_KEY, as atomic-chat-env.ts's comment claims?
Credential access
[file request container/pull.sh] From which registry or URL does container/pull.sh acquire the agent image, and how is it verified (digest pinning, lock-label check) before being retagged to the local build tag?
Supply chain
[file request container/install-cli-tools.sh] What does the overlay's install-cli-tools.sh install from cli-tools.json, and does it fetch from pinned official registries or arbitrary URLs?
Supply chain
[file request container/pull.sh] build.sh delegates registry image acquisition to this script — which registry or URL does pull.sh fetch the agent image from, how is the fetched image verified (lock-label check), and does any downloaded content execute at build or host time?
Supply chain
[file request container/install-cli-tools.sh] The overlay build copies and runs install-cli-tools.sh with cli-tools.json — what does it install, from which sources, and does it execute unpinned remote content during the image build?
Supply chain
[file request .claude/skills/add-atomic-chat-tool/atomic-chat-mcp-stdio.ts] What do the atomic_chat MCP tools execute or send when invoked — do they confine requests to ATOMIC_CHAT_HOST (default localhost:1337) and return only model responses, or do they read or transmit anything else from the host or container?
Code execution
[file request .claude/skills/add-atomic-chat-tool/atomic-chat-env.ts] What does atomicChatEnv() read from the host environment and forward into the container's contributed env — only ATOMIC_CHAT_HOST and ATOMIC_CHAT_API_KEY, or any other variables?
Credential access
[file request container/agent-runner/src/db/messages-in.ts] Where do task messages carrying content.script originate — host operator scheduling only, or can chat participants inject scripts that applyPreTaskScripts executes with bash?
Code execution
[file request container/pull.sh] container/build.sh hands off to pull.sh to acquire the agent image; which registry and tag does pull.sh pull from, is the image source pinned/trusted, and does it execute any fetched content at install time?
Supply chain
[file request src/providers/index.ts] Do any provider host-container config adapters read hostEnv (process.env) and copy values from it into container env, mounts, or outbound requests, and is credential-value filtering applied downstream of the context built in resolveProviderContribution?
Credential access
[file request src/container-config.ts] What does validateAdditionalMounts allow for containerConfig.additionalMounts, and can additional mounts expose host paths beyond the group subtree?
Persistence
[file request src/drivers/docker-driver.ts] DockerSessionDriver is registered by SRC:3dcde55cccdd777f in src/drivers/index.ts and spawns agent session containers: what environment variables, mounts, or host material does it pass into or read around session containers beyond the mount policy and the two NANOCLAW_* settings read in src/drivers/index.ts, and is any credential or host secret routed into containers?
Credential access
[file request src/compact-instructions.ts] The PreCompact hook injected into the agent's settings.json runs 'bun /app/src/compact-instructions.ts'; does that script only insert compaction-related instructions, or does it also read local files, send data off-host, or modify configuration?
Code execution
[file request container/CLAUDE.md] What standing instructions does the canonical instruction template contain that the host renders into every agent's composed CLAUDE.md?
Agent hijacking
[file request src/container-config.ts] Does the host re-validate add_mcp_server submissions and enforce admin approval before writing container.json and restarting the container?
Persistence
[file request src/drivers/docker-driver.ts] DockerSessionDriver is imported and pre-registered as the default session driver; do its container mounts stay within the declared MountPolicy surfaceRoots and does its spawn path read host credentials or send data off-host beyond the egress-network arguments built by dockerNetworkArgs?
Code execution
[file request container/CLAUDE.md] Does the canonical instruction template direct the container agent to run commands, fetch remote resources, read credential paths, or conceal actions from the user?
Code execution
[file request src/providers/atomic-chat.ts] If this provider file exists, does its registerProviderContainerConfig call pass host environment variables (tokens/secrets) into the container, mount host paths beyond defaults, or fetch anything from a remote URL at spawn time?
Credential access
Refuted findings 15

Candidates the review stage examined and rejected, kept for transparency.

Credential access

criticalRefutedhigh confidence

The skill's atomicChatEnv() helper copies the host's credential-named ATOMIC_CHAT_API_KEY (and ATOMIC_CHAT_HOST) verbatim into the container's contributed env lane — a lane chosen, per its own comment, because the composed lane's key-name check would refuse the spawn — and the helper performs no value validation itself.

env.ATOMIC_CHAT_API_KEY = process.env.ATOMIC_CHAT_API_KEY;
Why it was refuted

The helper copies only the two documented Atomic Chat variables into the platform's designed contributed lane, whose credential-value check the artifact's own comment acknowledges is preserved; this is the env-API-key-for-wrapped-service carve-out and the lane choice is openly documented in the code, not concealed.

Data exfiltration

criticalRefutedmedium confidence

The Atomic Chat MCP server sends full user and system prompts and model data to `${ATOMIC_CHAT_HOST}${apiPath}` where ATOMIC_CHAT_HOST is an environment variable that redirects all prompt/model traffic from the local default to any arbitrary remote host.

const url = `${ATOMIC_CHAT_HOST}${apiPath}`;
Why it was refuted

Sending the prompt to the configured Atomic Chat endpoint is the tool's declared function, the host is operator-set configuration with a documented local default, and the artifact never selects a remote host itself — posting prompts to the configured model endpoint is the declared core capability, not exfiltration.

Data exfiltration

criticalRefutedlow confidence

The Atomic Chat MCP server posts agent-supplied prompts and, when set, the ATOMIC_CHAT_API_KEY bearer token to whatever host ATOMIC_CHAT_HOST resolves to (default host.docker.internal:1337 with a localhost fallback), so a non-default configured host receives conversation content.

const url = `${ATOMIC_CHAT_HOST}${apiPath}`;
Why it was refuted

The bearer token is the wrapped service's optional API key sent to that service's own endpoint, whose default is the documented local Atomic Chat host; routing to an operator-configured ATOMIC_CHAT_HOST is openly documented configuration for the declared capability, not an artifact-chosen exfil sink.

Code execution

criticalRefutedlow confidence

The skill mandates that the agent run ./container/build.sh plus pnpm build/tsc/vitest/bun test commands as a required validation step, rebuilding the agent container image during skill application.

./container/build.sh
Why it was refuted

Running the platform's own build/test entry points after applying the skill's declared edits is the declared install workflow — running ./container/build.sh is itself a declared claim (D23) — so this is the declared core capability, not hidden or hostile exec.

Persistence

criticalRefutedlow confidence

The skill instructs the agent to modify the agent platform's own source (MCP server registration in container index.ts, an atomicChatEnv spread inside composeSessionSpec, a Docker driver stderr patch, env stubs, service restart) and ships structural tests that fail if this wiring is removed, entrenching the changes in the agent control plane until REMOVE.md is run.

`atomic-chat-registration.test.ts` asserts this entry is present and points at the server module
Why it was refuted

The source edits are the skill's declared install (D13/D9), and the structural tests are drift detectors run in the declared validation phase — they go red when the wiring is removed and restore nothing, so they are change detection, not an entrenching persistence mechanism.

Tool poisoning

criticalRefutedlow confidence

extendTool lets installed modules additively attach new parameters, passthrough keys, and description suffixes to already-registered MCP tools, and withOutboundPassthrough silently merges those extension-supplied entries into kind:'system' outbound payloads without the writing handler knowing, while the host-side handlers consuming the injected keys are not shown in this content.

add params to a base tool and have them land in the tool's outbound
Why it was refuted

The passthrough is an openly documented extension seam whose entries come from the calling agent's own tool args, apply only to kind:'system' JSON payloads, and can never overwrite keys the handler wrote — the described silent merge is the declared design with explicit scope limits, not concealment or poisoning.

Data exfiltration

criticalRefutedlow confidence

Undeclared by the skill's stated purpose: the runner's send_file tool stages any agent-readable file (absolute paths allowed, not confined to the workspace) into /workspace/outbox for host delivery to a named chat destination, giving the agent a one-call path to move local files off the container.

fs.copyFileSync(resolvedPath, path.join(outboxDir, filename));
Why it was refuted

send_file is the platform's declared messaging tool whose sink is limited to operator-configured destinations (unknown names are refused by resolveRouting) with host-side permission enforcement, so the file movement targets the operator's own chats, not an attacker-controlled endpoint.

Persistence

criticalRefutedlow confidence

On every spawn the host composes an agent instruction file into the group folder and mounts it into the container at /workspace/agent/CLAUDE.md, a write to an agent rules/memory file whose inlined instruction sources are not visible in the shown units.

containerPath: projectDocument?.containerPath ?? '/workspace/agent/CLAUDE.md',
Why it was refuted

The evidence shows the opposite of a persistence channel: the instruction document is recomposed from host-side sources at every spawn and mounted with readonly: true, so neither the container agent nor a stale write can persist instructions through /workspace/agent/CLAUDE.md; the remaining concern rests only on sources not shown, which is an absence claim.

Persistence

criticalRefutedlow confidence

The claude provider contract declares writing settings.json into the agent's group-scoped .claude state volume with content from the unshown constant CLAUDE_DEFAULT_SETTINGS and replacing existing settings via the claudeSettingsTransformer, an agent control-plane write whose content and reconciliation effect cannot be verified from the shown units.

content: CLAUDE_DEFAULT_SETTINGS,
Why it was refuted

The claim's premise is contradicted by shown units: CLAUDE_DEFAULT_SETTINGS is fully defined in SRC:da6ac0e185470a0c (memory-disable flags plus the PreCompact hook) and the claudeSettingsTransformer reconciliation is shown in SRC:a26dc3914f030af9, so the write's content and reconciliation effect are verifiable from the shown units; the residual hook-payload question is carried by the migrate-claude-memory-settings findings.

Privilege escalation

highRefutedmedium confidence

SKILL.md instructs the agent to edit composeSessionSpec to spread atomicChatEnv() into the contributed-env lane specifically because ATOMIC_CHAT_API_KEY is credential-named and the composed lane's key-name check would refuse the spawn — deliberately routing a credential-named variable through the lane that exempts the credential-name safety check.

the contributed lane exempts the name and still refuses credential-shaped values
Why it was refuted

The forwarded variable is the wrapped Atomic Chat service's own optional API key (env-API-key-for-wrapped-service carve-out), and the platform's composeSessionSpec documents the contributed lane as its designed name-check-exempt lane whose credential-VALUE checks still apply; the skill declares this rationale verbatim (D16), so the claimed check evasion is declared core behavior.

Privilege escalation

highRefutedlow confidence

Registering the atomic_chat MCP server automatically derives and applies the mcp__atomic_chat__* agent allow-pattern, so applying the skill inflates the agent's allowed tools without per-tool operator approval.

Registering the server is enough to expose its tools
Why it was refuted

Exposing atomic_chat_list_models and atomic_chat_generate via the derived mcp__atomic_chat__* allow-pattern is the skill's declared core capability, and the derivation is stated openly in the same paragraph — this is the declared mechanism, not stealth permission inflation.

Supply chain

highRefutedmedium confidence

The skill instructs the agent to run ./container/build.sh during validation and troubleshooting, triggering a rebuild or registry pull of the agent image whose acquisition path (pull.sh) is not agent-consumed content and cannot be reviewed.

./container/build.sh
Why it was refuted

The served build.sh shows the skill only ever instructs the bare invocation, and PULL is enabled solely by the explicit `pull` subcommand; the bare path performs a local docker build/overlay of the checkout's own content, so the flagged unreviewable pull.sh acquisition path is never executed by the instructed action, and the rebuild step is openly declared in the skill itself (D23), failing both harm and undeclared.

Supply chain

highRefutedmedium confidence

SKILL.md instructs copying its own four source files into both the host (src/) and container (container/agent-runner/src/) runtime trees and editing agent-runner registration (index.ts), spawn env composition (composeSessionSpec), and the Docker driver's stderr logger, then rebuilding the image and restarting the host service — beyond merely adding a container.json MCP entry — with the installed MCP server source not part of the shown content.

This skill reaches into both the container (Bun) tree and the host (Node) tree, so its
Why it was refuted

Copying the shipped sources into both trees, registering in index.ts, and forwarding env in container-runner.ts are declared verbatim as the skill's install capability (D9), and the claim that the installed MCP server source is not shown is factually wrong — the full sources of atomic-chat-mcp-stdio.ts and atomic-chat-env.ts are present in the shown units.

Privilege escalation

highRefutedlow confidence

The container runner reads CA certificates from NODE_EXTRA_CA_CERTS and installs them into Chromium's NSS trust database (~/.pki/nssdb) with 'C,,' root trust flags, making the agent's browser trust the TLS-inspecting gateway as a root CA for all browser traffic in the container.

run('certutil', ['-A', '-d', db, '-n', `nanoclaw-gateway-ca-${i}`, '-t', 'C,,', '-i', file]);
Why it was refuted

The code installs nothing on its own — it returns 0 unless the operator set NODE_EXTRA_CA_CERTS — and only mirrors into the per-container, ephemeral Chromium NSS db a CA the operator already designated for trust in the same environment, so the artifact grants no new trust anchor or privilege.

Credential access

highRefutedmedium confidence

composeSessionSpec composes contributedEnv from adapter and gateway env with no filtering in the composer itself — adapters receive hostEnv: process.env and their contribution lands in the lane exempt from the credential-name check, so credential-shaped adapter-contributed values pass composition unless an unseen downstream value check refuses them.

exempt from the credential-NAME check and still refused credential VALUES
Why it was refuted

The finding's harm was conditioned on an 'unseen downstream value check' refusing credential-shaped contributedEnv values; the served src/drivers/types.ts shows that check exists and runs: validateSpec refuses credential VALUES in contributedEnv from any source (name check exempted, value check not), and the composer itself declares the lane contract, so credential-shaped adapter/gateway-contributed env cannot carry into the container.

Coverage gaps 11

Parts of the artifact the static analysis could not fully examine.

context:unanalyzable_binary binary content is only byte-scanned
.claude/skills/add-clidash/add/tools/clidash/public/favicon.ico
unanalyzable_binary binary content is only byte-scanned
src/drivers/docker-driver.ts
parse_error tree-sitter typescript parse has errors; extraction may be partial
src/db/sessions.ts
parse_error tree-sitter typescript parse has errors; extraction may be partial
src/types.ts
dynamic_metadata line 37: tool (computed name) tools list depends on unsupported construct (unsupported): array 'allTools' may be changed in place
container/agent-runner/src/mcp-tools/server.ts
no_dataflow_for_dynamic_code line 17: invoked code is not structurally extracted
container/install-cli-tools.sh
no_dataflow_for_dynamic_code line 26: invoked code is not structurally extracted
container/install-cli-tools.sh
ledger_finding_without_valid_finding run 1: behavior-chains.3 SRC:aecde70c77154958
invalid_judge_finding behavior-chains.3
request_unanswered request.1: declared-vs-actual.1/browser-trust: finding: no valid finding cites the answer's evidence
invalid_judge_finding request.2