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

add-ollama-tool

skill:nanocoai/nanoclaw#add-ollama-tool

Add Ollama MCP server so the container agent can call local models and optionally manage the Ollama model library.

Verdict
Suspicious

Risky behavior the analysis could not rule out.

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

Dispute security verdict

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

25
findings
high
highest severity
6
threat classes
1,343
files scanned
2,447/2,449
checks satisfied
0
package advisories

Why this verdict

  • suspicious: finding skill:.claude/skills/add-ollama-tool:container/agent-runner/src/mcp-tools/server.ts:51:TOOLPOISON
  • suspicious: finding skill:.claude/skills/add-ollama-tool:container/agent-runner/src/mcp-tools/server.ts:78:TOOLPOISON
  • suspicious: finding skill:.claude/skills/add-ollama-tool:container/agent-runner/src/memory/scaffold.ts:9:PERSIST
  • suspicious: finding skill:.claude/skills/add-ollama-tool:container/agent-runner/src/memory/session-hook.ts:14:PERSIST
  • suspicious: finding skill:.claude/skills/add-ollama-tool:container/agent-runner/src/memory/session-hook.ts:15:PERSIST
  • suspicious: finding skill:.claude/skills/add-ollama-tool:container/agent-runner/src/poll-loop.ts:188:EXFIL
  • suspicious: finding skill:.claude/skills/add-ollama-tool:container/agent-runner/src/poll-loop.ts:221:EXEC
  • suspicious: finding skill:.claude/skills/add-ollama-tool:container/agent-runner/src/provider-contracts/claude.ts:64:PERSIST
  • suspicious: finding skill:.claude/skills/add-ollama-tool:container/agent-runner/src/provider-contracts/realize.ts:45:CRED
  • suspicious: finding skill:.claude/skills/add-ollama-tool:container/agent-runner/src/provider-contracts/realize.ts:77:CRED
  • suspicious: finding skill:.claude/skills/add-ollama-tool:container/agent-runner/src/providers/claude-history.ts:63:CRED
  • suspicious: finding skill:.claude/skills/add-ollama-tool:container/agent-runner/src/scheduling/task-script.ts:210:EXEC
  • suspicious: finding skill:.claude/skills/add-ollama-tool:container/agent-runner/src/scheduling/task-script.ts:26:EXEC
  • suspicious: finding skill:.claude/skills/add-ollama-tool:container/agent-runner/src/upload-trace.ts:11:EXFIL
  • suspicious: finding skill:.claude/skills/add-ollama-tool:container/agent-runner/src/upload-trace.ts:77:EXFIL
  • suspicious: finding skill:.claude/skills/add-ollama-tool:src/container-runner.ts:1627:EXEC
  • suspicious: finding skill:.claude/skills/add-ollama-tool:src/container-runner.ts:1:SUPPLY
  • suspicious: finding skill:.claude/skills/add-ollama-tool:src/migrate-claude-memory-settings.ts:122:PERSIST
  • suspicious: finding skill:.claude/skills/add-ollama-tool:src/migrate-claude-memory-settings.ts:53:PERSIST
  • suspicious: finding skill:.claude/skills/add-ollama-tool:src/migrate-claude-memory-settings.ts:8:PERSIST
  • suspicious: finding skill:.claude/skills/add-ollama-tool:src/modules/mount-security/index.ts:51:CRED
  • suspicious: finding skill:.claude/skills/add-ollama-tool:src/project-doc-compose.ts:245:TOOLPOISON
  • suspicious: finding skill:.claude/skills/add-ollama-tool:src/project-doc-compose.ts:84:PERSIST
  • suspicious: finding skill:.claude/skills/add-ollama-tool:src/provider-contracts/claude.ts:44:PERSIST
  • suspicious: unverified finding skill:.claude/skills/add-ollama-tool:container/agent-runner/src/poll-loop.ts:187:EXFIL

Security findings 25

Tool poisoning

highContextual riskmedium confidence

The extendTool seam lets installed feature modules additively modify registered base tools' input schemas and descriptions and copy call-argument keys verbatim into outbound system-action payloads the host processes, without touching base tool source.

Arg keys copied verbatim from the call args into any system-action
vector:code
Counter-argument considered

The extendTool seam, its passthrough wrapper, and the decorateContent sink all exist verbatim in the shipped runner, and no carve-out or artifact mitigation neutralizes the additive tool-metadata/payload mutation; with no declared claims beyond the Ollama description it is undeclared but accurately reported.

Tool poisoning

highContextual risklow confidence

extendTool in mcp-tools/server.ts lets any imported module append to an already-registered tool's description, merge extra input-schema properties, and copy additional call arguments verbatim into outbound payloads, so the tool metadata an agent finally sees depends on which modules are installed and cannot be fully reviewed statically.

def.tool.description = def.tool.description ? `${def.tool.description} ${descriptionSuffix}` : descriptionSuffix;
vector:code
Counter-argument considered

The extendTool implementation matches the quoted line exactly, mutates descriptions, schemas, and outbound payloads, and the folded metadata unit confirms the final tool list cannot be fully resolved statically; nothing in the evidence defeats the claim.

Persistence

highContextual riskmedium confidence

At boot the runner writes a memory scaffold into the host-backed /workspace/agent so the files persist across ephemeral containers, and registers a session-start hook (bun /app/src/memory/hook.ts) that re-injects this memory content into every new context window — persistence and recurring context injection undeclared by the declared Ollama-skill purpose.

the host-backed, RW group dir, so it persists across the
vector:code
Counter-argument considered

The module doc and ensureMemoryScaffold confirm boot-time writes into the host-backed /workspace/agent that persist across containers, and the hook constant plus memoryContextForSessionStart confirm recurring re-injection; the Ollama-only declared description covers none of this.

Persistence

highContextual risklow confidence

The Claude provider runtime writes a SessionStart entry into the agent's settings.json registering the command 'bun /app/src/memory/hook.ts', giving that script automatic execution at session start via the agent control plane.

command: 'bun /app/src/memory/hook.ts',
vector:code
Counter-argument considered

MEMORY_SESSION_HOOK carries exactly this command and writeMemorySessionHook writes it into the agent's settings.json SessionStart array via the realize.ts lifecycle call, so the registration claim is fully evidenced and undeclared by the Ollama-only purpose.

Persistence

highContextual riskmedium confidence

The runner writes a SessionStart hook into the Claude settings.json that executes 'bun /app/src/memory/hook.ts' at every new, cleared, or compacted session start, and the hook script's own behavior is not in the served content.

command: 'bun /app/src/memory/hook.ts',
vector:code
Counter-argument considered

Served code confirms the runner registers a SessionStart command hook 'bun /app/src/memory/hook.ts' into Claude settings.json for startup|clear|compact on every lifecycle; no declared claim covers a memory hook (declared scope is only the Ollama MCP server), the hook-shim carve-out cannot apply because the payload script is unconsumed and thus unvalidated, and the write→session-start-execution chain is concrete. The internal dedup of prior identical commands does not neutralize the persistence primitive.

Data exfiltration

highUnverifiedmedium confidence

When an inbound chat message matches the /upload-trace command, the runner calls uploadTrace(providerName), which reads the newest Claude session transcript from ~/.claude/projects and uploads that conversation trace to Hugging Face, sending conversation content off-host on command.

log('Uploading session trace to Hugging Face');
trigger:conditionalvector:code

Data exfiltration

highContextual riskmedium confidence

The container runner handles a mailbox '/upload-trace' command by calling uploadTrace() and logging 'Uploading session trace to Hugging Face', sending the agent's session trace to an external third-party service — a capability undeclared by the declared Ollama-skill purpose, contradicting the adjacent comment that the only command the runner handles directly is /clear.

log('Uploading session trace to Hugging Face');
trigger:conditionalvector:code
Counter-argument considered

Same served function confirms the inline /upload-trace branch sends the session transcript to Hugging Face (undeclared by the Ollama-only skill purpose) and directly contradicts the adjacent comment stating the runner's only directly-handled command is /clear; the upload-trace module's own comment ('the runner handles this command directly') shows the contradiction is real, not a misreading.

Code execution

highContextual riskmedium confidence

The runner dynamically imports scheduling/task-script.js and executes pre-task scripts attached to mailbox task rows for both initial batches and follow-up messages, running content sourced from the message database before each provider call — undeclared by the declared Ollama-skill purpose.

const { applyPreTaskScripts } = await import('./scheduling/task-script.js');
vector:code
Counter-argument considered

Both the initial-batch and follow-up paths dynamically import task-script.js and run applyPreTaskScripts over mailbox message rows before provider calls, which ends in spawn('bash', ...) — accurate, connected, and undeclared by the Ollama-only description.

Persistence

highContextual riskhigh confidence

The runner registers a Claude SessionStart hook by writing hooks.SessionStart into the Claude settings.json with command `bun /app/src/memory/hook.ts` (matcher startup|clear|compact, 10s timeout), executing at every new context window a file that is not part of the reviewable consumed content.

hooks: [{ type: 'command', command: hook.command, timeout: 10 }],
vector:code
Counter-argument considered

writeMemorySessionHook writes hooks.SessionStart with the hook command and a 10s timeout into the Claude settings.json, wired from the contract lifecycle through registerProviderMemorySessionHook; the matcher, command, and timeout all match the units and no declared claim covers it.

Credential access

highContextual risklow confidence

Undeclared residue R3 (unexplained, not concealed): newestClaudeTranscript enumerates ~/.claude/projects and returns the newest conversation transcript path, reading sensitive agent conversation history that the declared Ollama-MCP purpose does not cover.

dirs = fs.readdirSync(projects);
vector:code
Counter-argument considered

newestClaudeTranscript really enumerates ~/.claude/projects and returns the newest transcript path, it is flagged undeclared residue, and it is wired into the /upload-trace path via readTrace; nothing in the evidence contradicts the claim.

Code execution

highContextual riskhigh confidence

Pre-task scripts are taken verbatim from the script field of inbound task message rows, written to a /tmp file, and executed under bash in a detached process group with the container's full environment, so any party able to write task rows gains arbitrary command execution in the container.

const script = typeof content.script === 'string' ? (content.script as string) : null;
vector:code
Counter-argument considered

The script is taken verbatim from content.script of inbound task rows, written to /tmp, and spawned under bash with the full environment via runScript, reached from both poll-loop call sites; no carve-out applies and no declared claim covers DB-sourced command execution.

Code execution

highContextual riskhigh confidence

The agent-runner runs an arbitrary bash script embedded in each task message's content.script field, written to /tmp/task-script-<taskId>.sh and spawned under bash in a detached process group with the full process environment and no content validation, gating the task on the script's JSON wakeAgent output.

const child = spawn('bash', [scriptPath], { detached: true, env: process.env, stdio: ['ignore', 'pipe', 'pipe'] });
vector:code
Counter-argument considered

runScript writes the unvalidated message-supplied script to /tmp/task-script-<taskId>.sh and spawns bash detached with process.env exactly as quoted; the chain from task rows through applyPreTaskScripts is connected and the capability is undeclared by the Ollama-only description.

Data exfiltration

highContextual riskmedium confidence

The /upload-trace command handler sends the agent session's transcript to a remote Hugging Face dataset via curl through a proxy that injects the user's HF token, and its setup path deliberately widens the created secret's scope to all of huggingface.co.

upload this session's transcript to the user's
vector:code
Counter-argument considered

The flow is real and undeclared: the poll loop invokes uploadTrace on /upload-trace, which base64-reads the session transcript and commits it to huggingface.co, and nothing in the declared claims covers it; the admin gating and user's-own-private-dataset context lower severity but no carve-out or neutralizing mitigation is cited, so it stands.

Data exfiltration

highContextual riskhigh confidence

When the poll loop handles the admin-gated /upload-trace command, container/agent-runner/src/upload-trace.ts reads the session's provider transcript file and commits its base64 contents to a huggingface.co dataset ({user}/nanoclaw-traces) via the Hub commit API.

https://huggingface.co/api/datasets/${repo}/commit/main
trigger:conditionalvector:code
Counter-argument considered

The claim matches the code exactly: the admin-gated command path calls uploadTrace, which reads the provider transcript and POSTs its base64 contents to the HF Hub commit API; no declared claim covers this and no carve-out applies, so the finding stands as a contextual risk.

Code execution

highContextual riskmedium confidence

buildAgentGroupImage interpolates stored packages_apt and packages_npm values into a Dockerfile executed by docker build on the host without re-validating package-name syntax, so any stored value carrying shell or Dockerfile syntax would run as build-time commands on the host.

apt-get install -y ${aptPackages.join(' ')}
vector:code
Counter-argument considered

The claim is accurate on the shown code: stored JSON columns are parsed and interpolated unvalidated into a Dockerfile that docker build executes on the host; the operator-facing install_packages context and imageBuild capability gate reduce severity but nothing cited neutralizes the sink.

Persistence

highContextual risklow confidence

The migration writes a PreCompact command hook (bun /app/src/compact-instructions.ts) into the agent's settings.json during reconcile, an agent control-plane write that adds an executable hook to agent sessions.

preCompact.push({ hooks: [{ type: 'command', command: PRE_COMPACT_COMMAND }] });
vector:code
Counter-argument considered

Nothing in the evidence neutralizes this: the reconcile path deterministically pushes a command hook running bun /app/src/compact-instructions.ts into the agent's settings.json — not a carve-out, not covered by the declared Ollama-only description — and the written file is the state volume agent sessions consume, so the write→hook-execution chain is concrete.

Persistence

highContextual riskmedium confidence

The Claude provider contract and the host realization write and reconcile the container agent's auto-loaded settings.json (an agent control-plane file on a read-write mounted state volume), with content taken from the CLAUDE_DEFAULT_SETTINGS constant and a registered transformer whose write behavior is not shown in this bundle, leaving a host-to-agent-settings persistence chain whose payload is unverifiable here.

dir.writeNewFile(CLAUDE_SETTINGS_FILE, Buffer.from(CLAUDE_DEFAULT_SETTINGS));
vector:code
Counter-argument considered

Confirmed: host group init writes and reconciles the agent's auto-loaded settings.json from CLAUDE_DEFAULT_SETTINGS, which embeds a PreCompact command hook; the transformer's behavior is in fact shown in SRC:eacd595a87873785, which only corrects the claim's 'not shown' hedge and does not neutralize the undeclared persistence chain.

Persistence

highContextual riskmedium confidence

Residue adjudicated unexplained: host group initialization force-writes the Claude agent control-plane file settings.json, installing a PreCompact command hook that executes bundled code (bun /app/src/compact-instructions.ts) and forcing auto-memory off — an undeclared control-plane write the declared add-Ollama-MCP-server purpose does not need.

const PRE_COMPACT_COMMAND = 'bun /app/src/compact-instructions.ts';
trigger:conditionalvector:code
Counter-argument considered

The shown transformer installs the PreCompact command hook into the agent's settings.json and forces auto-memory off at group init; the hook installation is real, reachable, and covered by no declared claim of this Ollama-only skill, so the residue stands.

Credential access

highContextual risklow confidence

The mount validator applies its credential deny-list only to the resolved mount root and never descends, so an operator-allowed root such as the home directory mounts credential files beneath it (.ssh, .aws, .netrc, id_rsa) into the container despite the block list.

is mounted regardless of what this list says
vector:code
Counter-argument considered

The module's own SCOPE comment states the block list runs only on the mount root and never descends, confirming the claim verbatim; read-only-by-default and an agent-unwritable allowlist lower severity but do not neutralize the credential exposure beneath an operator-allowed root.

Tool poisoning

highContextual riskmedium confidence

composeGroupProjectDoc copies stored MCP server `instructions` verbatim into the agent-facing project document while the host's re-validation (sanitizeStoredMcpServers/parseMcpServerConfig) constrains that field only to be a string, so any stored, agent-influenced instruction text becomes standing system-prompt content with no content filter.

if (mcp.instructions) push(`MCP Server: ${name}`, mcp.instructions, true);
vector:code
Counter-argument considered

The composer copies stored MCP `instructions` verbatim into the agent-facing document while re-validation only type-checks the field as a string, and the code's own comment calls this text agent-influenced standing system-prompt content; no declared claim of the Ollama-only skill covers the channel.

Persistence

highContextual riskmedium confidence

Undeclared residue: the shipped NanoClaw project-doc composer writes a per-group CLAUDE.md agent instruction document on the host at every container start, an agent control-plane write that the declared add-an-Ollama-MCP-server purpose does not cover.

File written into the group directory, e.g. `CLAUDE.md`.
vector:code
Counter-argument considered

composeGroupProjectDoc writes groups/<folder>/CLAUDE.md on the host at every spawn (echoed by the detector lead flagging host_config_write 'CLAUDE.md' as undeclared), an agent control-plane write the add-Ollama-MCP-server description does not cover.

Persistence

highContextual riskmedium confidence

Undeclared residue: the shipped Claude provider contract has the host create-if-missing and then reconcile the container agent's settings.json from the constant CLAUDE_DEFAULT_SETTINGS (content not shown in this batch), an agent control-plane write the declared Ollama-only purpose does not cover.

relativePath: 'settings.json',
vector:code
Counter-argument considered

The contract declares settings.json with create-if-missing plus reconciliation and the realization code writes it at group init; the constant's content is shown in this bundle (SRC:5e106501e56d4941), which corrects the claim's hedge but leaves the undeclared agent control-plane write standing.

Credential access

mediumContextual risklow confidence

Undeclared residue R1 (unexplained, not concealed): resolveRuntimeConfiguration defaults its environment parameter to the full process.env, exposing all environment variables to any provider capability resolver although the declared Ollama-MCP purpose does not declare generic environment access.

environment: NodeJS.ProcessEnv = process.env,
vector:code
Counter-argument considered

The default parameter binding process.env exists exactly as quoted and is flagged undeclared residue, but the shown resolvers ignore the environment and no sink consumes it, so the chain gate fails only as absence — which can never refute.

Credential access

mediumContextual risklow confidence

Undeclared residue R2 (unexplained, not concealed): registerProviderMemorySessionHook resolves the provider memory capability with the full process.env, passing every environment variable into provider capability code although the declared Ollama-MCP purpose does not declare it.

resolveCapability(memoryCapability, hook, process.env)
vector:code
Counter-argument considered

registerProviderMemorySessionHook does resolve the memory capability with process.env as quoted and no declared claim covers it, but no shown consumer of that environment reaches a sink, so the chain gate fails as absence and cannot ground a refutation.

Supply chain

mediumContextual riskmedium confidence

The repository's derived-image build path appends the group's configured npm packages to /root/.npmrc to allowlist pnpm build (postinstall) scripts, enabling install-time code execution from those packages during image builds — an install-script supply surface unrelated to the add-ollama-tool skill's declared purpose of adding a local, keyless Ollama MCP server.

pnpm skips build scripts unless packages are allowlisted. Append each
Decoded
pnpm skips build scripts unless packages are allowlisted. Append each
    // to /root/.npmrc (base image sets it up for agent-browser) so packages
    // with postinstall — e.g. playwright, puppeteer, native addons — don't
    // install silently broken.
vector:code
Counter-argument considered

Strongest counter is that the packages are operator-configured and pnpm's default blocks scripts, but the shipped build path explicitly writes each configured npm package into /root/.npmrc so its postinstall executes during docker build — outside every carve-out, absent from the declared Ollama-MCP-only claims, and a concrete group-config→Dockerfile→postinstall-execution chain.

Open questions 54

What the analysis could not settle from the code alone.

SKILL.md documents OLLAMA_HOST as a free-form URL (e.g. http://your-ollama-host:11434); if it is pointed at a remote host, ollama_generate sends conversation prompts there — no validation or allowlisting of OLLAMA_HOST values is shown in the instruction text.
Data exfiltration
DET:61c05e22c22aef56 cites the string 'id_ed25519' at src/modules/mount-security/index.ts:81, but that excerpt is not visible in the decoded view of this file shown here; confirm whether it is an entry in DEFAULT_BLOCKED_PATTERNS (defensive blocklisting of SSH key paths from mounts) rather than a credential read.
Credential access
DET:4765819d19fec61d cites a file_read of '/workspace/agent/CLAUDE.md' at src/provider-contracts/claude.ts:15, which is outside the shown span (lines 37-83); confirm whether this is the host composing/reading the project document it owns (benign) or access to agent configuration beyond the declared task.
Credential access
DET:4fc6dda7f8cd3f32 cites a read of '/root/.npmrc' at container/install-cli-tools.sh:20; that file is not in this batch. Confirm whether the npm config read is benign tool installation setup or accesses registry credentials that then leave the host.
Credential access
DET:4b3c1fb99357dfd0 cites unrestricted process.env access at src/container-runner.ts:1135; that file is not in this batch. Confirm which environment variables are read and whether any credential value is threaded into containers or sent off-host.
Credential access
Does container/agent-runner/src/provider-contracts/realize.ts read environment variables beyond declared passthrough and pass any value to a network or storage sink?
Credential access
Does src/drivers/index.ts read process.env only for driver configuration, or does any value reach a credential or network sink?
Credential access
Does the allowlisted-extra pinning class applied to in-tree provider mount contributions (defined in src/drivers/types.ts) skip user approval for extra mounts that a skill-registered provider contributes?
Privilege escalation
None of the units shown implements the declared ollama_list_models/ollama_generate/ollama_pull_model/ollama_delete_model/ollama_show_model/ollama_list_running tools; whether the modules barrel (../modules/index.js) loads an Ollama tool module, and whether that module registers anything beyond the declared tools or calls extendTool on base tools, cannot be verified from this batch.
Tool poisoning
Whether the host-side delivery enforcement (src delivery.ts via the agent_destinations table) restricts which destinations or files send_file may deliver is asserted in comments but not shown, so the reach of the file-egress channel is undetermined.
Data exfiltration
The modules barrel comment says mailbox skills replace mailbox/compose.ts — can skills override runner source modules, and does this target exercise that replacement?
Supply chain
MODULE-HOOK:scheduling-pre-task markers imply a module injection step that fills the scheduling hook block at build or load time; which module supplies it and what code can it inject?
Code execution
All units in this batch come from container/agent-runner rather than the declared skill root .claude/skills/add-ollama-tool; confirm whether the skill invokes or ships this runner code.
readProviderTrace/newestClaudeTranscript feed a '/upload-trace' flow whose destination and handler are not in these units; whether transcript content leaves the host cannot be settled from this evidence.
Data exfiltration
The comment claims NanoClaw's container isolation and the OneCLI allow-list are the security boundary that makes bypassPermissions acceptable; that boundary is not verifiable from the units shown.
Privilege escalation
R21/C5: the cited 'network echo.ai' line is an echoed documentation URL (echo of build instructions), not an outbound request; no code in this batch contacts echo.ai and its status as the project's genuine publisher domain is unverifiable here — does any executed or fetched build code (pull.sh, Dockerfile, pnpm config) contact echo.ai?
Data exfiltration
R20: the detector reports file_read of $PROJECT_ROOT/.env.bak, but the cited line is 'rm -f' deleting the backup that 'sed -i.bak' just created on the previous line; no read of .env.bak is visible — confirm no other build step reads .env.bak.
Credential access
R23: the detector reports file_read of /root/.npmrc, but the cited code writes /root/.npmrc (writeFileSync of pnpm only-built-dependencies opt-ins) and never reads npm auth from it — confirm nothing else in the install flow reads credentials from /root/.npmrc.
Credential access
Where do task messages' script fields originate (host API, channel messages, peer agents)? This batch does not show the writer, so the reach of the runScript bash-execution sink cannot be established.
Code execution
The owed units for the add-ollama-tool skill contain no Ollama-specific code (only container-orchestration, DB, and messaging-host modules); whether the skill actually exercises these host modules, or only the declared Ollama MCP tools whose metadata units live in other batches, cannot be settled from this batch.
All 118 units in this batch are host db/migration/session/driver plumbing (messaging groups, approvals, migration runner, driver registry) with no Ollama- or MCP-related content, while the declared target is the add-ollama-tool skill; confirm whether these src/db and src/drivers files are actually consumed by the skill (which would make them large undeclared surface) or are unrelated repository context served with the batch.
Detector lead DET:3f1e60c3868e150e (RX-HIJACK-SECRECY, excerpt 'silently run') was checked against SRC:14ea2872d2b35661 and does not hold: the matched phrase is 'a host configured for one runtime that would otherwise silently run another', a rationale for rejecting driver fallbacks, not an instruction to act without the user's knowledge; confirm no other view of src/drivers/index.ts contains an actual secrecy directive.
Agent hijacking
The declared purpose is adding an Ollama MCP server, but every unit in this batch is host-runtime framework code (docker session driver selection, egress lockdown via the container CLI, gateway-provider approval registry, .env parsing, mount/credential validation) with no Ollama-specific logic; whether this framework is payload the skill ships and patches, or repository context outside the skill's payload, cannot be settled from the units shown.
Do residue items R10-R17 (file_read of .ssh, .gnupg, .aws, .netrc, .npmrc, .pypirc, id_rsa, id_ed25519) describe real credential reads? The cited literals are entries of DEFAULT_BLOCKED_PATTERNS, a mount denylist that rejects these host paths, so the claimed reads appear refuted; please confirm the detector classification is a false positive.
Credential access
Does residue R19 (file_read '/workspace/agent/CLAUDE.md') describe a real file read? The literal is a containerPath declaration in the Claude provider contract naming where the composed document is mounted inside the container, so the read appears refuted.
Credential access
Does the add-ollama-tool skill actually consume or modify the shipped NanoClaw src/ framework files (mount-security, typing, provider-contracts, project-doc-compose) shown in this batch, or are they unrelated repository content, given the target declares only adding an Ollama MCP server?
Whether the NanoClaw gateway whose CA is trusted into Chromium's NSS store is operator-controlled only, or whether its CA material can be influenced by a third party, which would make the trust-store import a MITM foothold rather than a functional requirement.
Persistence
What does trustGatewayCaForChromium write (which NSS/trust-store files inside the container) and where does the gateway CA certificate it installs come from?
Persistence
poll-loop.ts mentions running pre-task scripts for tasks such as monitoring crons; where do these scripts originate and what can they execute?
Code execution
Does the host router actually restrict /upload-trace to authorized operators before it reaches the container runner, as the comment claims?
Data exfiltration
Which inbound messages does categorizeMessage classify as passthrough/admin and pass raw to the SDK as native slash commands, and can a channel message trigger them?
Code execution
What does the OneCLI allow-list constrain in practice, given the agent runs with bypassPermissions and wildcard mcp__*__* allowedTools for every registered MCP server?
Privilege escalation
Who authors the task messages whose content.script field the container executes, and can an untrusted channel participant inject one?
Code execution
Is the claimed host-side admin gate on the /upload-trace command actually enforced before a message matching the command reaches the container?
Data exfiltration
Under what condition does build.sh reach the branch that rewrites NANOCLAW_HARDENED_IMAGE=false in the host .env, and can a skill cause that branch to run?
Persistence
What does the add-ollama-tool skill itself add to cli-tools.json or the Dockerfile, and does it instruct running container/build.sh (host-side rebuild with root-level installs) to apply the change?
Supply chain
Detector lead DET:cf0a5d82dc89a702 flags an EXFIL claim-contradiction ('Ollama runs locally and is keyless...') against src/config.ts:136, but that claim text does not appear in the shown unit, whose only content is local env-var validation via net.isIP with a 127.0.0.1 default and no network send; where does the claimed keyless/local Ollama statement live and does any Ollama path send data off-host?
Data exfiltration
Detector lead DET:3e94bf2295c4c523 classifies '/workspace/agent/CLAUDE.md' as a host_config_write, but the shown code mounts it read-only into the container from the group's data directory; confirm no host-side persistence write exists for this path outside the app's own composed project document.
Persistence
DET:3f1e60c3868e150e (HIJACK 'silently run') on src/drivers/index.ts:21 does not confirm: the phrase refers to a host that would 'otherwise silently run another' runtime driver due to fallback selection, and the code's stated design throws instead of falling back; it is not an instruction to act without the user's knowledge.
DET:3e328c1be01792db (CRED process.env) on src/drivers/index.ts:68 does not confirm as credential access: readSetting is typed to only the two NANOCLAW_RUNTIME_DRIVER / NANOCLAW_SESSION_MATERIAL_ROOT config keys, not arbitrary env or secrets.
No unit in this batch references Ollama, MCP server installation, or the declared add-ollama-tool capability; the shown units are host-internal DB and driver-selection code, so the link between the declared skill purpose and these files is not established by the evidence shown.
updateMessagingGroup, updateMessagingGroupAgent, and updateSession interpolate update object keys directly into SET clauses without the runtime column allowlist that updateContainerConfigScalars/updateContainerConfigJson apply; is any untrusted external content ever routed into these update objects at a callsite?
The declared target is the 'add-ollama-tool' skill providing Ollama MCP tools, but every unit in this batch is host-side infrastructure (provider-contract validators, provider container registry, session manager, reconcile feeds, timezone helpers, DB type comments) with no reference to Ollama, MCP servers, or the skill directory; confirm whether this content actually belongs to the declared skill artifact or is unrelated repository context.
extractAttachmentFiles, readOutboxFiles, and clearOutbox rely on AnchoredDir.open and isSafeAttachmentName/inboxFolderName/deriveAttachmentName, whose implementations are not in the shown lines; do these helpers actually enforce the described basename checks and symlink refusal, so the base64-decode-to-inbox-write chain cannot escape the session directory?
[invalid file request] How is the Ollama base URL resolved (OLLAMA_HOST default and localhost fallback), and are ollama_pull_model/ollama_delete_model/ollama_show_model/ollama_list_running registered only when OLLAMA_ADMIN_TOOLS=true as the comments claim?
Data exfiltration
[file request container/CLAUDE.md] This file is cited by SRC:84a50405ef7a851e ('container/CLAUDE.md is the canon') and SRC:1e2534e1bd6b1e5a as the core-owned canonical instruction template rendered into every agent's project document, i.e. text the agent will read. Does it contain directives that override agent instructions, demand secrecy, or steer beyond the declared task?
Agent hijacking
[file request container/build.sh] The skill instructs running ./container/build.sh after its code edits and in troubleshooting; what does this unshown build script execute, and does it run anything beyond rebuilding the container image?
Code execution
[file request container/agent-runner/src/memory/hook.ts] What does the SessionStart hook command 'bun /app/src/memory/hook.ts' read, write, or fetch over the network when it runs at session start?
Persistence
[file request container/agent-runner/src/mailbox/index.ts] Which mailbox implementation registers as the agent mailbox, and where do outbound message rows (including decorated system-action payloads) physically leave the container (socket, database, HTTP endpoint)?
Data exfiltration
[file request src/index.ts] Does host startup call initDb with a url-bearing DbConfig or install a non-SQLite/remote database driver via the composition barrel?
Data exfiltration
[file request .env.example] Which env vars are declared in .env.example, and do any carry credentials or reachable host URLs beyond the documented Ollama block?
Credential access
[file request container/pull.sh] Which registry and reference does pull.sh fetch the agent image from, and is the reference pinned or digest-verified before it is retagged locally?
Supply chain
[file request src/providers/index.ts] Which provider host-adapter functions consume context.hostEnv (process.env), and can any of them copy host environment values into contribution.env, mounts, or auxiliary-container config that reaches the agent container?
Credential access
[file request src/drivers/docker-driver.ts] How does DockerSessionDriver construct the container invocation (command, mounts, env, network args), and does any secret, host file, or session payload reach the container or an external sink?
Code execution
Refuted findings 26

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

Destructive action

criticalRefutedlow confidence

When OLLAMA_ADMIN_TOOLS=true the server registers ollama_delete_model and ollama_pull_model, letting the container agent delete models from or download models into the host's Ollama library behind a single opt-in env flag with no further confirmation beyond the skill's one-time user prompt.

'Delete a locally installed model from the Ollama daemon to free disk space.',
Why it was refuted

ollama_pull_model and ollama_delete_model are the declared 'optionally manage the Ollama model library' tools, registered only under OLLAMA_ADMIN_TOOLS, which the skill sets solely after asking the user — the container agent cannot enable them itself, and deleted models are re-pullable, so this is the declared, consent-gated capability.

Data exfiltration

criticalRefutedmedium confidence

The ollama_generate tool serializes the agent-supplied prompt into a POST body and sends it to the operator-configurable OLLAMA_HOST URL (default http://host.docker.internal:11434, custom host documented in SKILL.md) with no destination validation, so a non-local OLLAMA_HOST turns every model call into off-host exfiltration of conversation content.

return await fetch(url, options);
Why it was refuted

Posting the agent-supplied prompt to the configured Ollama daemon base URL is the tool's declared function (D10, POST /api/generate); the default destination is the local daemon and OLLAMA_HOST is set only by the operator in the documented .env, so a non-local host reflects operator configuration, not artifact exfiltration.

Persistence

criticalRefutedhigh confidence

The skill instructs the agent to edit composeSessionSpec's env literal to spread a host-env forwarding helper so OLLAMA_* variables from the host environment are injected into the container's session environment on every spawn.

find the `env` literal (the `TZ` line) and spread the helper right after it
Why it was refuted

The helper forwards exactly two Ollama configuration variables (ollamaEnv() copies only OLLAMA_HOST and OLLAMA_ADMIN_TOOLS, no wildcard), and the skill explicitly declares that it 'forwards host env vars in container-runner.ts' (D14) — this is the declared core capability, not covert persistence.

Code execution

criticalRefutedlow confidence

SKILL.md instructs the agent to install Ollama from https://ollama.com/download whenever the local daemon probe fails, conditionally fetching a remote vendor installer at runtime.

Install Ollama from https://ollama.com/download.
Why it was refuted

Fetching the official Ollama vendor installer, only when the local daemon probe fails, is the skill's declared setup step (D17 declares this exact sentence) — a vendor-installer prerequisite of the declared 'call local models' capability, not concealed runtime execution.

Persistence

criticalRefutedhigh confidence

The skill instructs the agent to copy its own MCP server source into both agent-runner runtime trees and add an ollama entry to the runner's mcpServers config so the server launches with the agent and its tools are auto-exposed via the derived mcp__ollama__* allow-pattern.

Add an `ollama` entry alongside `nanoclaw`:
Why it was refuted

Copying the skill's own MCP server source into the runner trees and registering it under mcpServers is precisely the declared capability (D14: 'copies them into the agent-runner tree at install time, then registers the server in index.ts'); the server source is fully served and its tools match the declared tool list, so nothing is hidden or out of scope.

Persistence

criticalRefutedlow confidence

trustGatewayCaForChromium reads the CA file named by NODE_EXTRA_CA_CERTS and registers it as a trusted CA in Chromium's NSS database via certutil, enabling the gateway to TLS-intercept the agent's browser traffic whenever that env var is set.

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

The routine imports only the CA that the standard NODE_EXTRA_CA_CERTS variable already designates for Node itself into a fresh per-container NSS database, so Chromium works behind the operator's own TLS gateway; it adds no new trust anchor and enables no interception the gateway did not already perform, and the purpose is openly documented in the file header.

Tool poisoning

criticalRefutedmedium confidence

Undeclared for a skill whose stated purpose is only adding an Ollama MCP server: the agent-runner ships a passthrough seam (withOutboundPassthrough/decorateContent, used by extendTool) by which an installed module can silently add entries to a base tool's outbound system-action payload without the writing handler knowing, enabling covert parameter injection into tool payloads.

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

The passthrough seam is openly documented in the served server header as the extension mechanism; decorations only add non-colliding keys to system-kind JSON payloads, sourced from the agent's own call arguments, default to a no-op, and never overwrite handler-written keys — granting nothing beyond an installed module's ordinary code execution.

Data exfiltration

criticalRefutedlow confidence

Undeclared relative to the skill's stated Ollama-only purpose, the send_file tool copies any agent-readable file, including absolute paths outside /workspace/agent, into an outbox directory for delivery to named messaging destinations, giving the container a generic file-egress channel whose destination gating is enforced only host-side (delivery.ts, not shown).

const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve('/workspace/agent', filePath);
Why it was refuted

send_file is the runner's documented core messaging tool: destinations are admin-wired ('You cannot send messages until an admin wires one up') and permission-enforced host-side (delivery.ts / agent_destinations table), so copying an agent-readable container file to the outbox for a named destination is designed delivery, not external exfiltration.

Persistence

criticalRefutedlow confidence

The container's self-modification MCP surface (add_mcp_server, per the module doc fire-and-forget writing a system action the host applies by updating container.json and restarting) accepts an agent-supplied arbitrary command with args and env as a new MCP server config, a config-level auto-start code-execution persistence channel whose host-side approval is asserted only in comments not shown here.

return { config: { command, args: commandArgs, env } };
Why it was refuted

The tool only validates and queues a system-action row; application is a host-side, admin-approved step asserted in the served module header — contradicting the claim that approval appears only in unserved comments — with HTTPS-only URLs, credential/secret-key rejection, and charset allowlists on input, and nothing executes from the container.

Persistence

criticalRefutedlow confidence

Undeclared for a skill whose purpose is adding a local Ollama MCP server, the add_mcp_server tool lets the agent request wiring an arbitrary third-party MCP server (any local command with args and env, or a remote HTTPS endpoint) into the container's config (container.json), a control-plane configuration write applied by a container restart on approval.

Wire an EXISTING third-party MCP server into YOUR per-agent runtime config.
Why it was refuted

add_mcp_server only submits a validated request row under an explicit 'Requires admin approval; fire-and-forget' contract, and the module header openly declares the self-modification surface, so the container.json write is human-gated on the host, not agent-applied persistence.

Persistence

criticalRefutedmedium confidence

The install_packages and add_mcp_server tools let the agent submit fire-and-forget system-action rows that, on host approval, rebuild the per-agent container image with new apt/npm packages or rewrite container.json MCP wiring, with approval and re-validation enforced only in host code not served here.

Install apt and/or npm packages into YOUR per-agent container image. Requires admin approval; fire-and-forget.
Why it was refuted

Both tools fire validated, approval-gated system-action rows only; the served header documents that the host processes them 'including admin approval' and re-validates host-side ('defense in depth'), and no container-side execution of packages or config changes exists in the served code.

Tool poisoning

criticalRefutedmedium confidence

The MCP server that serves this skill's tools computes the advertised tool list at runtime from a mutable registry that later-imported modules can mutate after registration (merged schema properties, appended descriptions, and verbatim argument passthrough into outbound payloads), so the client-visible tool surface is not fixed by reviewable static source and the declared static tool list is incomplete.

tools: allTools.map((t) => t.tool),
Why it was refuted

The tool surface is assembled at startup from the barrel's static imports via documented module-scope registerTools calls, extendTool mutations happen at import time before any request is served and throw on collisions, and the mechanism is openly documented — the declared metadata itself carries a '(not static)' entry, so the listing is neither concealed nor beyond reviewable source.

Privilege escalation

criticalRefutedhigh confidence

resolveClaudeExecutionPolicy in claude-config.ts unconditionally sets permissionMode 'bypassPermissions' and allowDangerouslySkipPermissions true for every spawned Claude agent, disabling the SDK's permission prompts with only a comment asserting container isolation as the boundary.

permissionMode: 'bypassPermissions',
Why it was refuted

The flagged behavior reduces to granting the contained spawned Claude agent broad permissions by disabling the SDK's interactive prompts — the accepted broad-permissions-alone carve-out — and the artifact itself states the container isolation and allow-list are the security boundary, so the harm gate fails and the finding dies.

Tool poisoning

criticalRefutedlow confidence

parseSkillSelection accepts arbitrary strings from the stored skills column without name validation, and syncSharedSkillLinks later uses those names as symlink entry names and interpolates them into the target `/app/skills/${skill}`, so a crafted stored name could redirect skill discovery unless AnchoredDir rejects it.

if (Array.isArray(parsed) && parsed.every((n) => typeof n === 'string')) return parsed;
Why it was refuted

The finding's own escape clause ('unless AnchoredDir rejects it') resolves against it: every entry name, including the symlink name, is funneled through entryPath, which throws on unsafe names before fs.symlinkSync runs, and since the target reuses the same validated name, a stored name can only ever produce a link named <safe> pointing at /app/skills/<safe>, so no crafted stored column can redirect skill discovery or escape the anchored root.

Credential access

criticalRefutedmedium confidence

src/container-runner.ts passes the entire host process environment (hostEnv: process.env) into provider container-config functions whose returned env merges into contributedEnv (a lane exempt from the credential-NAME check) and is injected into the session container, giving host environment variables a second and broader bridge into the container than the skill's ollamaEnv spread.

hostEnv: process.env,
Why it was refuted

Refuted on harm: handing process.env to in-tree, registry-sourced provider adapters is broad permissions alone, and the destination lane's own contract still refuses credential VALUES, so no credential bridge into the container is established by the shown evidence.

Credential access

criticalRefutedlow confidence

resolveProviderContribution passes the entire host process environment (hostEnv: process.env) into provider host-adapter functions whose returned env is injected into the agent container's contributedEnv, an undeclared env:* capability that could carry host secrets into containers if any adapter copies values through.

hostEnv: process.env,
Why it was refuted

Refuted on harm: the same hostEnv grant into in-tree host-adapter functions is broad permissions alone, and the merged contributedEnv lane's documented contract refuses credential VALUES, so the conditional 'could carry host secrets' claim has no supporting adapter behavior in the shown evidence.

Persistence

criticalRefutedmedium confidence

Undeclared for an Ollama MCP server skill (residue R5, adjudicated unexplained: plainly visible, not hidden), the host composes a CLAUDE.md agent-instruction document from all instruction sources on every spawn and mounts it into the container at /workspace/agent/CLAUDE.md, giving the host-side composer control over the agent's instructions.

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

Refuted on harm: the cited evidence shows the document is regenerated fresh on every spawn from the group's configured instruction sources and mounted read-only inside the group dir the container already mounts RW — the platform's core, plainly visible spawn composition, not persistence.

Code execution

highRefutedmedium confidence

SKILL.md instructs the agent to run ./container/build.sh, whose PULL branch executes container/pull.sh, content the repository marks NOT CONSUMED, so the code that runs at that path cannot be reviewed.

./container/build.sh
Why it was refuted

The skill's validation step invokes build.sh with no arguments, and in build.sh PULL becomes "true" only via an explicit pull|--pull argument while every branch of a bare invocation sets PULL="false" or exits 3, so the guarded sink (bash "$SCRIPT_DIR/pull.sh" "$TAG") is never reached from this instruction; the code actually run at the instructed path (build.sh) is fully shown here and benign (echo.ai is only a printed help URL and .env.bak is a sed backup removed in place).

Tool poisoning

highRefutedlow confidence

decorateContent merges entries from any installed module into system-action outbound payloads written by base tools without modifying those tools, an extension seam that can inject parameters into tool payloads but for which no malicious injector is shown in this pass.

payload[key] = value;
Why it was refuted

decorateContent is a documented, deliberately narrow seam (system-kind JSON only, never overwrites handler-written keys, no-op without an active context) and the finding itself concedes no injector module exists in the artifact, so there is no covert parameter-injection chain.

Agent hijacking

highRefutedlow confidence

Undeclared for the declared Ollama-setup purpose: the runner instructs the agent that <internal> blocks mark thinking it does not want sent and then strips those blocks from replies before delivery, giving the agent a channel to withhold arbitrary output from the user.

Strip `<internal>...</internal>` blocks from agent output, then trim.
Why it was refuted

Stripping agent-marked <internal> blocks is the platform's documented scratchpad filter, declared in the agent's own delivery instructions; the agent already controls every character it emits, so a self-marked strip tag grants no withholding capability beyond ordinary omission.

Supply chain

highRefutedlow confidence

Undeclared for a skill whose purpose is adding a local Ollama MCP server, the install_packages tool lets the container agent submit apt and npm installs by unpinned name only (no version pinning allowed), which on approval rebuilds the per-agent image and restarts the container, an agent-triggered package supply-chain surface.

On approval, the image is rebuilt and the container is restarted automatically.
Why it was refuted

Package requests are name-only (version specs and flags rejected), charset-validated, capped at 20, and applied only after explicit admin approval with documented host-side re-validation — the designed human-gated install workflow in which the container performs no installation itself, not an agent-driven supply chain.

Privilege escalation

highRefutedlow confidence

The Claude provider forwards an allowDangerouslySkipPermissions flag from the resolved execution policy into the SDK query, so when the policy enables it the agent runs with permission prompts bypassed; whether the policy can enable it is not established here.

allowDangerouslySkipPermissions: this.executionPolicy.allowDangerouslySkipPermissions,
Why it was refuted

This is the same contained-agent permission forwarding one hop downstream of the policy constant (which unconditionally sets it true), and its whole substance is broad SDK permissions for the sandboxed agent — the broad-permissions-alone carve-out — so the harm gate fails and the finding dies with F12.

Credential access

highRefutedlow confidence

Residue R7 is unexplained, not concealed: configuredDriverKind reads the NANOCLAW_RUNTIME_DRIVER setting from the host process environment, undeclared for the stated Ollama-MCP purpose, with the value only selecting a session driver locally.

export function configuredDriverKind(env: NodeJS.ProcessEnv = process.env): DriverKind {
Why it was refuted

The read touches one enumerated, non-secret local setting used only to pick the session driver (no credential or external sink), and the module header openly documents both the key and the env precedence, so neither harm nor undeclared survives and there is no chain.

Credential access

highRefutedlow confidence

Residue R8 is unexplained, not concealed: mountPolicy defaults its env parameter to process.env and reads NANOCLAW_SESSION_MATERIAL_ROOT from the host environment, undeclared for the stated Ollama-MCP purpose, with the value resolving a local mount path only.

export function mountPolicy(env: NodeJS.ProcessEnv = process.env): MountPolicy {
Why it was refuted

The value resolves a local session-materials path only — no credential leaves the host and no external sink exists — and the module header documents the key and the .env/process.env precedence, so the behavior is neither harmful nor concealed.

Credential access

highRefutedlow confidence

Residue R6 is unexplained, not concealed: the runtime code in this Ollama-MCP skill reads host process environment variables (with .env fallback) via readSetting, an environment-access capability the declared purpose never mentions, though the keyed values are used only for local configuration.

export function readSetting(key: (typeof SETTINGS)[number], env: NodeJS.ProcessEnv = process.env): string {
Why it was refuted

readSetting is typed to only the two enumerated non-secret driver settings and consumes them locally; the module header openly declares the env-reading behavior, leaving no credential surface, no concealment, and no source→sink chain.

Credential access

highRefutedlow confidence

Residue R9 adjudicated unexplained: gateway-provider selection code reads the undeclared environment variable NANOCLAW_GATEWAY_PROVIDER from process.env and an env file (the detector's 'env *' is over-broad since only this one named key is read), which the declared add-Ollama-MCP-server purpose does not plainly require.

env.NANOCLAW_GATEWAY_PROVIDER?.trim()
Why it was refuted

Only the single named key is read and it merely selects among locally registered gateway providers with no external sink, and the artifact itself presents the key as an operator-facing setting in its own error guidance, so harm and undeclared both fail and no chain exists.

Coverage gaps 14

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_unknown run 1: instruction-surface.0 SRC:5211abca396348f
ledger_missing run 1: instruction-surface.0 SRC:5211abca39637fc5
ledger_finding_without_valid_finding run 1: behavior-chains.3 SRC:b1cdc5bbb5b3bd6d
invalid_judge_finding behavior-chains.3
request_unanswered instruction-surface.0: invalid file request (evidence_ids not shown in the asking request)
request_unanswered request.2: declared-vs-actual.0/req-container-build: finding: no valid finding cites the answer's evidence
invalid_judge_finding request.2