# Defense-in-Depth Score: marimo

**Repo:** https://github.com/marimo-team/marimo · **Commit:** `82b936f910b644c6b8052b22e4083b77a1b716b7` · **Reviewed:** 2026-10-03
**What it is:** Reactive Python notebook with built-in AI agent chat and MCP server
**Category:** Data & Analytics
**Scored configuration:** `marimo edit` local server with an AI provider configured, chat panel in its tool-using modes (Agent mode, and Code Mode which the 'Fix with AI' button selects and saves automatically); the shipped default Manual chat mode exposes no tools; no MCP servers configured by the user; marimo's own --mcp server and the external-agents panel not scored.
**Agent surface (default):** code execution yes · filesystem write yes · network egress yes · external credentials yes · persistent memory no · untrusted input yes · third party extensions opt-in · sub agents no · external communication yes

## Score: 1.2 / 10.0 (Minimal)

| # | Criterion | S | C | D | B | Raw | Cap | Score | Confidence |
|---|---|---|---|---|---|---|---|---|---|
| C1 | Identity & least privilege | L0 | L0 | L0 | L0 | 0.00 | — | **0.00** | High |
| C2 | Approval gates | L0 | L0 | L0 | L1 | 0.05 | C2-POWERBYPASS | **0.05** | High |
| C3 | Tool & action scoping | L0 | L1 | L1 | L0 | 0.12 | — | **0.12** | High |
| C4 | Code-execution isolation | L0 | L0 | L0 | L0 | 0.00 | — | **0.00** | High |
| C5 | Untrusted input blast radius | L0 | L0 | L0 | L0 | 0.00 | C5-WORSTCASE | **0.00** | High |
| C6 | Memory, context & configuration integrity | L0 | L1 | L1 | L1 | 0.17 | C6-REPOCONFIG | **0.17** | High |
| C7 | Third-party extensions | L0 | L0 | L0 | L1 | 0.05 | C7-RCELOAD | **0.05** | High |
| C8 | Secrets & sensitive-data protection | L1 | L1 | L2 | L0 | 0.25 | — | **0.25** | High |
| C9 | Audit & traceability | L1 | L2 | L2 | L1 | 0.38 | — | **0.38** | High |
| C10 | Limits & kill switch | L1 | L1 | L1 | L0 | 0.20 | — | **0.20** | High |


Once the chat is in Agent or Code Mode, marimo's assistant runs the code it writes in your notebook kernel immediately, as you, with no approval, sandbox or step limit. The 'Fix with AI' button on any cell error switches the chat to Code Mode and keeps it there. A prompt injection in a shared notebook, a dataset or a web page can therefore read your credentials (including the AI keys marimo hands the kernel) and send them anywhere, or delete files, unattended. A cloned repo's pyproject.toml can also make marimo launch MCP server commands at startup.

## Critical gaps
- Agent-written code runs as the user in the notebook kernel with the user's full environment and marimo's unmasked AI keys, so a hijacked session holds all of the user's authority. (ASI03, T3; C1) — [marimo/_session/managers/kernel.py:77](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_session/managers/kernel.py#L77); [marimo/_session/managers/kernel.py:90](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_session/managers/kernel.py#L90)
- The assistant's most powerful actions, running model-written code in the kernel via execute_code or run_stale_cells, execute with no human approval. (ASI09, ASI02, T10, LLM06; C2) — [marimo/_server/ai/tools/code_mode.py:36-50](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/ai/tools/code_mode.py#L36-L50); [frontend/src/core/ai/tools/run-cells-tool.ts:99-104](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/frontend/src/core/ai/tools/run-cells-tool.ts#L99-L104); [frontend/src/components/chat/chat-panel.tsx:669-680](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/frontend/src/components/chat/chat-panel.tsx#L669-L680)
- Model-written code runs unsandboxed in the host kernel process as the user, with environment credentials, the unmasked marimo config and full network. (ASI05, T11, LLM05; C4) — [marimo/_session/managers/kernel.py:77](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_session/managers/kernel.py#L77); [marimo/_session/managers/kernel.py:90](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_session/managers/kernel.py#L90); [marimo/_server/ai/tools/code_mode.py:36-50](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/ai/tools/code_mode.py#L36-L50)
- After prompt injection via notebook code, data outputs, MCP results or web pages, the assistant can both exfiltrate secrets and take irreversible actions through kernel code with no human step. (ASI01, T6, LLM01; C5) — [marimo/_server/ai/prompts.py:308](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/ai/prompts.py#L308); [marimo/_server/ai/tools/code_mode.py:36-50](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/ai/tools/code_mode.py#L36-L50); [frontend/src/components/chat/chat-panel.tsx:596](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/frontend/src/components/chat/chat-panel.tsx#L596)
- A cloned repository's pyproject.toml can add MCP stdio servers that marimo launches at startup and redirect the AI provider base URL, with no trust prompt. (ASI06, T1, ASI04; C6) — [marimo/_config/manager.py:306-309](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_config/manager.py#L306-L309); [marimo/_server/api/lifespans.py:135](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/api/lifespans.py#L135); [marimo/_server/ai/mcp/transport.py:80](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/ai/mcp/transport.py#L80)
- Code Mode lets the model install arbitrary packages without confirmation, and project config can make marimo launch MCP server commands at startup without consent. (ASI04, T17, LLM03; C7) — [marimo/_code_mode/_packages.py:113-133](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_code_mode/_packages.py#L113-L133); [marimo/_server/api/lifespans.py:135](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/api/lifespans.py#L135); [marimo/_server/ai/mcp/transport.py:80](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/ai/mcp/transport.py#L80)

## Criterion details

### C1 Identity & least privilege — 0.00 (high)

marimo's assistant acts with the full authority of the person who started marimo. Code the agent writes or runs executes in the notebook kernel, a child process of the server running as the same OS user that also receives the unmasked marimo configuration (including AI provider keys), and MCP servers are started with a copy of the whole environment. There is no dedicated identity, no scoped credential and no per-tool authorization layer; the only check is the server's login token, which identifies the human user, not what the agent may do. A hijacked session therefore has every file, credential and network path the user has.

- **S L0:** Kernel code (the agent's execution target) runs in a same-user child process that is handed the unmasked config; nothing narrows the operator's ambient authority. — [marimo/_session/managers/kernel.py:77](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_session/managers/kernel.py#L77); [marimo/_session/managers/kernel.py:90](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_session/managers/kernel.py#L90) (verified)
  - *To reach the next level:* No dedicated or narrowed identity: the kernel and MCP subprocesses get the user's full credentials and environment.
- **C L0:** No tool path uses a scoped identity: execute_code, run_stale_cells and MCP stdio servers all run with ambient credentials. — [marimo/_server/ai/tools/code_mode.py:36-50](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/ai/tools/code_mode.py#L36-L50); [marimo/_server/ai/mcp/transport.py:75-83](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/ai/mcp/transport.py#L75-L83) (verified)
  - *To reach the next level:* Not even the main tool path passes through an authorization layer or narrowed credential.
- **D L0:** The default install runs everything as the launching user with all their credentials; no least-privilege default exists. — [marimo/_session/managers/kernel.py:77](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_session/managers/kernel.py#L77); [marimo/_server/ai/mcp/transport.py:75-83](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/ai/mcp/transport.py#L75-L83) (verified)
  - *To reach the next level:* No narrower default identity exists to fall back on.
- **B L0:** Model-written Python in the kernel can reach everything the user can: files, cloud/SSH credentials in the environment, the marimo server auth token and network. — [marimo/_session/managers/kernel.py:90](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_session/managers/kernel.py#L90); [marimo/_server/scratchpad.py:387](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/scratchpad.py#L387) (verified)
  - *To reach the next level:* Nothing limits the reach to a project or read-only scope.
- **Cap:** none

### C2 Approval gates — 0.05 (high)

There is no approval step for any assistant action. In Agent mode the model's edits are applied to cells and its run_stale_cells tool then executes those edited cells immediately; in Code Mode the execute_code tool runs arbitrary Python in the kernel; MCP tools are invoked directly by the server. Tool calls are executed as soon as the model emits them and the chat automatically sends the result back for the next step. Edited and deleted cells are kept as 'staged' changes with the previous code so the user can reject them afterwards, but by then the code has already run.

- **S L0:** Frontend tool calls run automatically in onToolCall and backend/MCP tools run inside the agent run; no human approval is requested. — [frontend/src/components/chat/chat-panel.tsx:669-680](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/frontend/src/components/chat/chat-panel.tsx#L669-L680); [frontend/src/components/chat/chat-utils.ts:139-150](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/frontend/src/components/chat/chat-utils.ts#L139-L150); searched `rg -n -i 'requires_approval|needs_approval|ApprovalRequired'` in `marimo/` → 0 hits (No backend tool, MCP tool or code-mode execute_code call is ever marked as needing approval; the frontend ToolApprovalCard only renders when a server emits an approval request, which the notebook assistant never does.) (verified)
  - *To reach the next level:* No per-call human approval of commands, code or MCP calls.
- **C L0:** The most powerful paths (execute_code, run_stale_cells, MCP tools) are all ungated. — [marimo/_server/ai/tools/code_mode.py:36-50](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/ai/tools/code_mode.py#L36-L50); [frontend/src/core/ai/tools/run-cells-tool.ts:99-104](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/frontend/src/core/ai/tools/run-cells-tool.ts#L99-L104); [marimo/_server/ai/tools/tool_manager.py:246-248](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/ai/tools/tool_manager.py#L246-L248) (verified)
  - *To reach the next level:* No tool path crosses an approval gate.
- **D L0:** There is no approval mechanism to turn on; the chat auto-resubmits after every tool result. — [frontend/src/components/chat/chat-panel.tsx:596](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/frontend/src/components/chat/chat-panel.tsx#L596); searched `rg -n -i 'requires_approval|needs_approval|ApprovalRequired'` in `marimo/` → 0 hits (No backend tool, MCP tool or code-mode execute_code call is ever marked as needing approval; the frontend ToolApprovalCard only renders when a server emits an approval request, which the notebook assistant never does.) (verified)
  - *To reach the next level:* Approval does not exist in any configuration.
- **B L1:** Cell edits and deletions are staged with the previous code and can be rejected, but kernel code effects (file deletion, network sends, package installs) are irreversible. — [frontend/src/core/ai/tools/edit-notebook-tool.ts:137-142](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/frontend/src/core/ai/tools/edit-notebook-tool.ts#L137-L142); [marimo/_server/ai/tools/code_mode.py:36-50](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/ai/tools/code_mode.py#L36-L50) (verified)
  - *To reach the next level:* No checkpoint or rollback for kernel side effects and no previews for external actions.
- **Cap:** C2-POWERBYPASS — Code execution in the kernel (execute_code in Code Mode, run_stale_cells in Agent mode), the most powerful action path, runs with no approval.

### C3 Tool & action scoping — 0.12 (high)

The agent's main tools are general-purpose: Code Mode's execute_code takes any Python string, and Agent mode can write any cell and then run it. The read-only backend tools have typed arguments that are parsed before use, but that does not constrain the code-execution paths, and MCP tool arguments are only checked by the MCP server itself. The shipped default chat mode is Manual with no tools, but the 'Fix with AI' button silently switches the chat to Code Mode and saves that choice, and a project's pyproject.toml can set the mode too.

- **S L0:** execute_code accepts an arbitrary Python string and run_stale_cells runs arbitrary model-written cells; the typed parse of backend read-only tool arguments does not bound these. — [marimo/_server/ai/tools/code_mode.py:36-50](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/ai/tools/code_mode.py#L36-L50); [frontend/src/core/ai/tools/run-cells-tool.ts:99-104](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/frontend/src/core/ai/tools/run-cells-tool.ts#L99-L104) (verified)
  - *To reach the next level:* No allowlist or bounds on what the execution tools may do; general tools are not replaced by narrow ones.
- **C L1:** Backend tools validate typed arguments before running; MCP tool calls and the execution tools pass through unvalidated. — [marimo/_server/ai/tools/tool_manager.py:218-229](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/ai/tools/tool_manager.py#L218-L229); [marimo/_server/ai/tools/tool_manager.py:246-248](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/ai/tools/tool_manager.py#L246-L248) (verified)
  - *To reach the next level:* No shared validation layer covering MCP and execution tools.
- **D L1:** Default mode is Manual (no tools), but exec-capable Code Mode is selected and persisted automatically by the 'Fix with AI' action, and MCP tools are exposed in Ask and Agent modes. — [frontend/src/core/config/config-schema.ts:170](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/frontend/src/core/config/config-schema.ts#L170); [frontend/src/components/editor/chrome/wrapper/useOpenAiAssistant.ts:18](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/frontend/src/components/editor/chrome/wrapper/useOpenAiAssistant.ts#L18); [frontend/src/components/editor/chrome/wrapper/useOpenAiAssistant.ts:76](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/frontend/src/components/editor/chrome/wrapper/useOpenAiAssistant.ts#L76); [marimo/_server/ai/tools/tool_manager.py:180-184](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/ai/tools/tool_manager.py#L180-L184) (verified)
  - *To reach the next level:* Exec tools can be enabled without an explicit user choice (Fix with AI, project config); no per-task tool allowlist.
- **B L0:** A misused execute_code call can do anything the user can on the machine and network. — [marimo/_server/ai/tools/code_mode.py:36-50](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/ai/tools/code_mode.py#L36-L50); [marimo/_session/managers/kernel.py:77](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_session/managers/kernel.py#L77) (verified)
  - *To reach the next level:* No workspace or quantity bound on the execution tools.
- **Cap:** none

### C4 Code-execution isolation — 0.00 (high)

Model-generated code runs directly in the notebook's Python kernel, which is a normal child process on the host running as the user. There is no container, OS sandbox or restricted interpreter for AI-driven execution; the 30-second timeout and interrupt only bound how long the server waits. The kernel has the user's home directory, environment credentials, the marimo config with API keys, and unrestricted network. MCP stdio servers are likewise host processes.

- **S L0:** The kernel is a spawn-context multiprocessing Process on the host with no isolation primitive. — [marimo/_session/managers/kernel.py:77](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_session/managers/kernel.py#L77); searched `rg -n -i 'sandbox|seccomp|landlock|nsjail|gvisor|firecracker'` in `marimo/_session/managers/kernel.py` → 0 hits (The edit-mode kernel launcher applies no isolation primitive.) (verified)
  - *To reach the next level:* No OS-level or runtime isolation for model-driven code.
- **C L0:** Neither execute_code, run_stale_cells nor MCP stdio launches go through any sandbox. — [marimo/_server/ai/tools/code_mode.py:36-50](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/ai/tools/code_mode.py#L36-L50); [frontend/src/core/ai/tools/run-cells-tool.ts:99-104](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/frontend/src/core/ai/tools/run-cells-tool.ts#L99-L104); [marimo/_server/ai/mcp/transport.py:80](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/ai/mcp/transport.py#L80) (verified)
  - *To reach the next level:* No execution path is sandboxed.
- **D L0:** No sandbox exists to enable for the AI execution paths. — searched `rg -n -i 'sandbox|seccomp|landlock|nsjail|gvisor|firecracker'` in `marimo/_session/managers/kernel.py` → 0 hits (The edit-mode kernel launcher applies no isolation primitive.) (verified)
  - *To reach the next level:* Isolation is not available, on or off.
- **B L0:** Code runs host-equivalent: full filesystem, environment credentials, unmasked marimo config and network. — [marimo/_session/managers/kernel.py:90](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_session/managers/kernel.py#L90); [marimo/_server/scratchpad.py:387](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/scratchpad.py#L387) (verified)
  - *To reach the next level:* No workspace-only mount, secret scrubbing or egress restriction.
- **Cap:** none
- **Notes:** Not credited: the CLI offers to run notebooks opened from a remote URL (or with --untrusted) in a stock Docker container (marimo/_cli/run_docker.py). It does not apply to local notebooks (the local file is not mounted), and runs as root with full network; its server configuration is not locked down.

### C5 Untrusted input blast radius — 0.00 (high)

Nothing limits what a prompt-injected assistant can do. The notebook's code (which may come from a shared or downloaded notebook) is placed in the system prompt, cell outputs from whatever data the notebook loads, MCP tool results and, when web search is turned on, fetched web pages all reach the model with the same standing as the user. The same session can then run arbitrary code with network access and the user's credentials, so an attacker can both steal data and take irreversible actions without any human step.

- **S L0:** No taint tracking, approval-after-untrusted-read or quarantine; nothing in the AI code distinguishes untrusted content. — searched `rg -n -i 'injection|untrusted'` in `marimo/_server/ai marimo/_ai` → 0 hits (No provenance tagging, taint tracking or untrusted-content handling in the AI subsystem.); [marimo/_server/ai/prompts.py:308](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/ai/prompts.py#L308) (verified)
  - *To reach the next level:* No structural limit (approval or capability removal) once untrusted content is read.
- **C L0:** Notebook code, cell outputs, MCP results and web pages all enter context undistinguished. — [marimo/_server/ai/prompts.py:308](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/ai/prompts.py#L308); [marimo/_server/ai/tools/tool_manager.py:246-248](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/ai/tools/tool_manager.py#L246-L248); [marimo/_server/ai/providers.py:499-500](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/ai/providers.py#L499-L500) (verified)
  - *To reach the next level:* No untrusted source is handled differently.
- **D L0:** No control exists to be on by default. — searched `rg -n -i 'injection|untrusted'` in `marimo/_server/ai marimo/_ai` → 0 hits (No provenance tagging, taint tracking or untrusted-content handling in the AI subsystem.) (verified)
  - *To reach the next level:* No untrusted-input control is present.
- **B L0:** A hijacked session can exfiltrate data and secrets through kernel code with network access and also delete or modify files, unattended. — [marimo/_server/ai/tools/code_mode.py:36-50](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/ai/tools/code_mode.py#L36-L50); [frontend/src/core/ai/tools/run-cells-tool.ts:99-104](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/frontend/src/core/ai/tools/run-cells-tool.ts#L99-L104); [frontend/src/components/chat/chat-panel.tsx:596](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/frontend/src/components/chat/chat-panel.tsx#L596) (verified)
  - *To reach the next level:* Exfiltration and irreversible actions are not gated by a human.
- **Cap:** C5-WORSTCASE — In Agent and Code Mode a hijacked assistant can leak data and take irreversible actions through kernel code with no human involved.

### C6 Memory, context & configuration integrity — 0.17 (high)

marimo has no agent memory store, but its configuration travels with projects. The nearest pyproject.toml's [tool.marimo] section overrides the user's own settings with only signing and cache-verification keys removed, so a cloned repository can add MCP servers whose commands marimo launches at startup, point the AI provider at its own base URL, set the chat mode or inject custom rules into every prompt, with no trust prompt. A .marimo.toml found by walking up from the working directory is loaded as the user's config. marimo does block AI and MCP settings in a notebook's own inline script header, which shows the maintainers recognise the risk.

- **S L0:** Repo-controlled pyproject.toml config is honoured for ai and mcp sections and its MCP stdio servers are launched at server start without any prompt. — [marimo/_config/manager.py:306-309](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_config/manager.py#L306-L309); [marimo/_config/reader.py:136-139](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_config/reader.py#L136-L139); [marimo/_server/api/lifespans.py:116-117](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/api/lifespans.py#L116-L117); [marimo/_server/api/lifespans.py:135](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/api/lifespans.py#L135); [marimo/_config/manager.py:69-75](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_config/manager.py#L69-L75) (verified)
  - *To reach the next level:* No workspace-trust decision before project config can add MCP servers or change AI endpoints.
- **C L1:** Only the PEP 723 notebook-header layer is allowlisted to exclude ai and mcp; pyproject.toml and a discovered .marimo.toml are not. — [marimo/_config/reader.py:62-66](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_config/reader.py#L62-L66); [marimo/_config/manager.py:638-643](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_config/manager.py#L638-L643) (verified)
  - *To reach the next level:* pyproject.toml and workspace .marimo.toml layers are uncontrolled.
- **D L1:** Chat history is kept per browser in localStorage, but project configuration is shared by everyone who opens the repository and outranks user settings. — [frontend/src/core/ai/state.ts:13](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/frontend/src/core/ai/state.ts#L13); [marimo/_config/manager.py:306-309](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_config/manager.py#L306-L309) (verified)
  - *To reach the next level:* No isolation of project-supplied config from the user's own security-relevant settings.
- **B L1:** A poisoned pyproject.toml (which Code Mode can itself write) persists across the user's sessions and starts processes or redirects credentials on every launch. — [marimo/_server/api/lifespans.py:135](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/api/lifespans.py#L135); [marimo/_server/ai/mcp/transport.py:75-83](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/ai/mcp/transport.py#L75-L83) (verified)
  - *To reach the next level:* Poisoned config is neither reviewed nor reversible before it takes effect.
- **Cap:** C6-REPOCONFIG — A workspace pyproject.toml can add MCP servers launched at startup and redirect the AI base URL without any user trust decision.

### C7 Third-party extensions — 0.05 (high)

marimo can launch MCP servers as local commands or connect to remote ones, with no pinning, hashes or change detection, and project config can add them silently. In Code Mode the model can install packages from the package index with a single call and no confirmation, and package installs run third-party install code. MCP stdio servers run as separate processes but receive a full copy of the user's environment.

- **S L0:** Code Mode exposes ctx.packages.add for model-chosen package installs, and MCP stdio commands are run unpinned with no verification. — [marimo/_code_mode/_packages.py:113-133](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_code_mode/_packages.py#L113-L133); [marimo/_server/ai/mcp/transport.py:80](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/ai/mcp/transport.py#L80); searched `rg -n -i 'sha256|integrity|signature'` in `marimo/_server/ai/mcp` → 0 hits (No pinning, hash or signature verification of MCP servers.) (verified)
  - *To reach the next level:* No pinning, integrity check or curated registry for packages or MCP servers.
- **C L0:** Neither packages nor MCP servers (stdio or remote) are verified. — searched `rg -n -i 'sha256|integrity|signature'` in `marimo/_server/ai/mcp` → 0 hits (No pinning, hash or signature verification of MCP servers.); [marimo/_server/ai/mcp/transport.py:80](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/ai/mcp/transport.py#L80) (verified)
  - *To reach the next level:* No extension type is verified.
- **D L0:** A repository's pyproject.toml can add MCP servers that are launched silently at startup. — [marimo/_config/manager.py:306-309](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_config/manager.py#L306-L309); [marimo/_server/api/lifespans.py:135](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/api/lifespans.py#L135) (verified)
  - *To reach the next level:* Workspace files can add extensions without consent.
- **B L1:** MCP stdio servers run as separate processes, same user, with a full copy of the environment. — [marimo/_server/ai/mcp/transport.py:75-83](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/ai/mcp/transport.py#L75-L83) (verified)
  - *To reach the next level:* No environment scrubbing or per-extension sandbox.
- **Cap:** C7-RCELOAD — In Code Mode the model can install arbitrary packages (running their install code) with no consent, and project config launches MCP commands without consent.

### C8 Secrets & sensitive-data protection — 0.25 (high)

AI provider keys live in the user's marimo TOML config (or are read from environment variables) and are masked before the config is sent to the browser. But the kernel, where agent-written code runs, is handed the configuration with secrets unmasked, and it inherits the full environment; Code Mode additionally places the marimo server's auth token into the kernel request. There is no redaction of tool results before they go to the model. marimo ships no third-party telemetry and AI tracing is off unless MARIMO_TRACING is set.

- **S L1:** API keys are masked in config sent to the frontend; secrets otherwise sit in plaintext config/env with no redaction elsewhere. — [marimo/_config/secrets.py:48-55](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_config/secrets.py#L48-L55); searched `rg -n -i 'redact|mask'` in `marimo/_server/ai` → 0 hits (No redaction of secrets in tool results or model-bound messages in the AI server code.) (verified)
  - *To reach the next level:* No redaction before logs and model-bound messages, no keychain or secret manager.
- **C L1:** Only the config-to-browser path is masked; the kernel, subprocess environments and tool results are not. — [marimo/_config/secrets.py:48-55](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_config/secrets.py#L48-L55); [marimo/_session/managers/kernel.py:90](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_session/managers/kernel.py#L90) (verified)
  - *To reach the next level:* Model-bound messages, kernel and subprocess environments are unprotected.
- **D L2:** No telemetry SDK ships; OpenTelemetry tracing of AI calls is opt-in via MARIMO_TRACING. — searched `rg -n -i 'posthog|sentry_sdk'` in `marimo/` → 0 hits (No third-party telemetry or crash reporter in the Python package.); [marimo/_config/settings.py:17](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_config/settings.py#L17) (verified)
  - *To reach the next level:* No always-on redaction; transcripts are not minimised.
- **B L0:** Long-lived provider keys and the server auth token are reachable by model-written kernel code. — [marimo/_session/managers/kernel.py:90](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_session/managers/kernel.py#L90); [marimo/_server/scratchpad.py:387](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/scratchpad.py#L387) (verified)
  - *To reach the next level:* Keys are long-lived and broadly reachable rather than scoped and short-lived.
- **Cap:** none

### C9 Audit & traceability — 0.38 (high)

The only record of what the assistant did is the chat transcript, which the browser saves to its local storage with every tool call's arguments and results once a response finishes. The server does not log successful tool calls or executed code, and OpenTelemetry tracing is opt-in. The transcript lives in the user's browser profile, can be deleted from the UI, and is written only when a turn completes, so a crash mid-run can lose it; code that agent-run cells spawn is not recorded at all.

- **S L1:** The browser-side chat transcript keeps each tool call's arguments and output, but tool parts carry no timestamps and there is no server-side record. — [frontend/src/core/ai/state.ts:88](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/frontend/src/core/ai/state.ts#L88); [frontend/src/components/chat/chat-panel.tsx:643-650](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/frontend/src/components/chat/chat-panel.tsx#L643-L650) (verified)
  - *To reach the next level:* No per-call timestamps or structured server-side record of tool calls.
- **C L2:** Frontend, backend and MCP tool calls all appear as tool parts in the transcript; there are no approvals to record and no server-side log. — [frontend/src/core/ai/state.ts:88](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/frontend/src/core/ai/state.ts#L88); searched `rg -n -i 'requires_approval|needs_approval|ApprovalRequired'` in `marimo/` → 0 hits (No backend tool, MCP tool or code-mode execute_code call is ever marked as needing approval; the frontend ToolApprovalCard only renders when a server emits an approval request, which the notebook assistant never does.) (verified)
  - *To reach the next level:* Approvals/denials and kernel side effects are not recorded; no server-side record.
- **D L2:** On by default in browser localStorage outside the workspace, but deletable from the UI and writable by the same user's processes. — [frontend/src/core/ai/state.ts:13](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/frontend/src/core/ai/state.ts#L13); [marimo/_config/settings.py:17](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_config/settings.py#L17) (verified)
  - *To reach the next level:* Not written by a component the model's code cannot control.
- **B L1:** State is saved in onFinish after the turn completes; nothing is flushed per action. — [frontend/src/components/chat/chat-panel.tsx:643-650](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/frontend/src/components/chat/chat-panel.tsx#L643-L650) (verified)
  - *To reach the next level:* Records are not flushed per action.
- **Cap:** none

### C10 Limits & kill switch — 0.20 (high)

marimo sets no step, time or cost budget for the assistant. Each code-mode execution is waited on for 30 seconds and then interrupted, and MCP calls time out after 30 seconds by default, but in Agent mode the chat automatically sends a new request after every tool result with no cap, and max_tokens is unset unless configured. Stopping the chat ends the stream, and in Code Mode cancels and interrupts the kernel, but processes or threads started by that code keep running, and cells started by run_stale_cells continue.

- **S L1:** Only per-execution timeouts exist (30 s for execute_code with kernel interrupt, 30 s MCP default); marimo passes no step, token or cost budget. — [marimo/_server/scratchpad.py:40-42](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/scratchpad.py#L40-L42); [marimo/_server/scratchpad.py:415-421](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/scratchpad.py#L415-L421); searched `rg -n 'UsageLimits|usage_limits|request_limit'` in `marimo/` → 0 hits (marimo never passes a step/request/token budget to its pydantic-ai agents.) (verified)
  - *To reach the next level:* No iteration cap or wall-clock/token/cost budget enforced by marimo.
- **C L1:** Timeouts apply to individual tool calls; the frontend auto-resubmit loop is unbounded. — [frontend/src/components/chat/chat-panel.tsx:596](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/frontend/src/components/chat/chat-panel.tsx#L596); [marimo/_server/ai/mcp/config.py:113-118](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/ai/mcp/config.py#L113-L118) (verified)
  - *To reach the next level:* No limit on the top-level loop or on spawned processes.
- **D L1:** Timeout defaults exist but steps, tokens and spend are unlimited by default. — searched `rg -n 'UsageLimits|usage_limits|request_limit'` in `marimo/` → 0 hits (marimo never passes a step/request/token budget to its pydantic-ai agents.); [marimo/_server/ai/config.py:440-443](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/ai/config.py#L440-L443) (verified)
  - *To reach the next level:* No sensible default budget for steps, time or cost.
- **B L0:** A runaway Agent-mode loop can keep acting and spending indefinitely; stop does not kill processes the code spawned. — [frontend/src/components/chat/chat-panel.tsx:596](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/frontend/src/components/chat/chat-panel.tsx#L596); [marimo/_server/scratchpad.py:415-421](https://github.com/marimo-team/marimo/blob/82b936f910b644c6b8052b22e4083b77a1b716b7/marimo/_server/scratchpad.py#L415-L421) (verified)
  - *To reach the next level:* No ceiling on steps or spend, and no process-group kill on stop.
- **Cap:** none

## Rule-of-Two check
[A] untrusted input: Notebook code in the system prompt (marimo/_server/ai/prompts.py:308), cell outputs, MCP results, optional web fetch (marimo/_server/ai/providers.py:500) · [B] sensitive data/systems: Kernel holds unmasked config with AI keys and the user's environment (marimo/_session/managers/kernel.py:90) · [C] state change / egress: execute_code runs arbitrary Python with network (marimo/_server/ai/tools/code_mode.py:36); run_stale_cells executes edited cells (frontend/src/core/ai/tools/run-cells-tool.ts:99) · Same default session? Yes

## Highest-impact improvements
1. Require per-call user approval showing the exact code before execute_code and run_stale_cells run (and before MCP tool calls). — C2 S L0→L3, +0.225 before caps (Playbook 5)
2. Apply the PEP 723 allowlist (no ai, mcp, completion sections) to pyproject.toml and workspace .marimo.toml, or ask for a workspace trust decision first. — C6 S L0→L3, +0.225 before caps (Playbook 2)
3. Stop passing the unmasked config (AI keys) to the kernel and scrub the environment passed to MCP stdio servers. — C8 B L0→L2, +0.100 before caps (Playbook 4)
4. Cap automatic tool round-trips per user message and pass a UsageLimits budget to every agent run. — C10 S L1→L2, +0.075 before caps (Playbook 3 step 3)
5. Ask for confirmation before Code Mode package installs, and do not switch the chat to Code Mode automatically from 'Fix with AI'. — C7 S L0→L1, +0.075 before caps (Playbook 3)

## Re-audit log
- No changes.

## Limitations
- Static source review of the pinned commit only; nothing was executed, installed, or probed.
- Scored the chat panel's tool-using modes (Agent and Code Mode). The shipped default chat mode is Manual, which sends no tools; Code Mode is labelled beta in the UI but is selected automatically by the 'Fix with AI' action.
- Not scored: marimo's opt-in MCP servers (`--mcp`, including a code-execution MCP server for external agents), the feature-flagged external agents (ACP) panel, mo.ui.chat user-built chatbots, molab hosting, and WASM/pyodide mode.
- pydantic-ai library defaults (per-run request limit) and DOMPurify/Streamdown remote-image handling were not verified in library source; no rating depends on them.
- No reviewer-steering text was found in the repository (AGENTS.md/CLAUDE.md contain developer guidelines only).
