with one click
marimo-pair
// Work inside a running marimo notebook's kernel — execute code, create cells, and build a notebook as an artifact. Use when the user wants to start a marimo notebook or work in an active marimo session.
// Work inside a running marimo notebook's kernel — execute code, create cells, and build a notebook as an artifact. Use when the user wants to start a marimo notebook or work in an active marimo session.
| name | marimo-pair |
| description | Work inside a running marimo notebook's kernel — execute code, create cells, and build a notebook as an artifact. Use when the user wants to start a marimo notebook or work in an active marimo session. |
| allowed-tools | Bash(bash **/scripts/discover-servers.sh *), Bash(bash **/scripts/execute-code.sh *), Read |
This skill gives you full access to a running marimo notebook. You can read cell code, create and edit cells, install packages, run cells, and inspect the reactive graph — all programmatically. The user sees results live in their browser while you work through bundled scripts or MCP.
marimo notebooks are a dataflow graph — cells are the fundamental unit of computation, connected by the variables they define and reference. When a cell runs, marimo automatically re-executes downstream cells. You have full access to the running notebook.
pyproject.toml, existing cells,
and dir(ctx) before reaching for external tools.Only servers started with --no-token register in the local server registry
and are auto-discoverable — starting without a token makes discovery easier.
If a server has a token, set the MARIMO_TOKEN environment variable before
calling the execute script (avoids leaking the token in process listings). The
right way to invoke marimo depends on context (project
tooling, global install, sandbox mode). See
finding-marimo.md for the full decision tree.
Do NOT use --headless unless the user asks for it. Omitting it lets
marimo auto-open the browser, which is the expected pairing experience. If the
user explicitly requests headless, offer to open it with
open http://localhost:<port>.
Two operations: discover servers and execute code.
| Operation | Script | MCP |
|---|---|---|
| Discover servers | bash scripts/discover-servers.sh | list_sessions() tool |
| Execute code | bash scripts/execute-code.sh -c "code" | execute_code(code=..., session_id=...) tool |
| Execute code (multiline) | bash scripts/execute-code.sh <<'EOF' | same |
| Execute code (direct URL) | bash scripts/execute-code.sh --url URL -c "code" | same (with url param) |
Scripts auto-discover sessions from the registry on disk. Use --port to
target a specific server when multiple are running, --session to target a
specific session when multiple notebooks are open on the same server, or
--url to skip discovery entirely and hit a server URL directly (e.g.
--url http://localhost:2718). --url is the only way to connect to
remote servers since auto-discovery only reads the local registry. Only
use --url with trusted servers — data is sent to the endpoint, so a
malicious URL could exfiltrate notebook contents. Set the MARIMO_TOKEN
env var to authenticate when the server has token auth enabled (--token
flag also works but exposes the token in process listings). If the
server was started with --mcp, you'll have MCP tools available as an
alternative.
Only --no-token servers are in the registry. If discovery comes up empty,
the server likely has token auth — ask the user for the token and set it as
the MARIMO_TOKEN environment variable.
Always discover before starting. Background task "completed" notifications do not mean the server died — check the output or run discover first.
If no servers are found, read the user's intent — if they want a notebook,
start one. Always start marimo as a background task (using
run_in_background on the Bash tool) so the server automatically gets cleaned
up when the session ends and doesn't block the conversation. See
finding-marimo.md.
If there's no .py file yet, pick a descriptive filename based on context
(e.g., exploration.py, analysis.py, dashboard.py). Don't ask — just
pick something reasonable.
Avoid shell escaping issues. -c works for simple one-liners, but for
multiline code or code with quotes/backticks/${}, use a heredoc or a file:
# heredoc (single-quoted delimiter prevents shell interpolation)
bash scripts/execute-code.sh <<'EOF'
import marimo._code_mode as cm
async with cm.get_context() as ctx:
ctx.create_cell("x = 1")
EOF
# file
bash scripts/execute-code.sh /tmp/code.py
# direct URL (skips auto-discovery and works with remote servers)
bash scripts/execute-code.sh --url http://localhost:2718 -c "1 + 1"
Every execute-code call runs inside the notebook's kernel. All cell variables
are in scope — print(df.head()) just works. Nothing you define persists
between calls (variables, imports, side-effects all reset), but you can freely
introspect the notebook: inspect variables, test code snippets, check types
and shapes. Use this to explore, prototype, and validate before committing
anything to the notebook — then create cells to persist state and make results
visible to the user.
To mutate the notebook's dataflow graph — create, edit, and delete cells,
install packages, and run cells — use marimo._code_mode:
import marimo._code_mode as cm
async with cm.get_context() as ctx:
cid = ctx.create_cell("x = 1")
ctx.install_packages("pandas")
ctx.run_cell(cid)
You must use async with — without it, operations silently do nothing.
All ctx.* methods are synchronous — they queue operations and the
context manager flushes them on exit. Do not await them.
Cells are not auto-executed. create_cell and edit_cell are structural
changes only — use run_cell to queue execution.
code_mode is a tested, safe API for notebook mutations — prefer it for all
structural changes. You also have access to marimo internals from the kernel,
but treat that as a last resort and only with high confidence after exploration.
UI state lives outside the reactive graph. Anywidget traitlets can be read
or set directly (e.g., slider.value = 5). For mo.ui.* elements, use
ctx.set_ui_value(element, new_value) inside code_mode.
The code_mode API can change between marimo versions — and each running
server could be a different version. Inspect what's available at the start of
each session, especially when switching between servers.
import marimo._code_mode as cm
async with cm.get_context() as ctx:
ctx # inspect me — dir(), help(), .cells, ...
Skip these and the UI breaks:
ctx.install_packages(), not uv add or pip.
The code API handles kernel restarts and dependency resolution correctly.
Only fall back to external CLIs if the API is unavailable or fails.mo.ui is fine for simple forms and controls.
See rich-representations.md..py file directly while a session is running — the kernel owns it.pathlib.Path("/tmp/...") in cell code is a bug.edit_cell into existing empty cells rather
than creating new ones. Clean up any cells that end up empty after edits.Anywidget state (traitlets) lives outside marimo's reactive graph. To hook a widget trait into the graph, pick one strategy per widget — never mix them:
mo.state + .observe() — you pick specific traits to bridge. Default choice.mo.ui.anywidget() — wraps all synced traits into one reactive .value. Convenient but coarser.Read rich-representations.md before wiring either.
ctx.install_packages() adds
real dependencies — confirm when it's not obvious from context.