notebooks — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited notebooks (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
A single skill for authoring, validating, and delivering reproducible analysis notebooks. Marimo is the default format; Jupyter is supported for existing .ipynb files and when a downstream tool requires JSON. Conversion between the two formats is part of this skill.
A notebook is not "done" until it has been executed end-to-end on a fresh kernel and every figure is embedded in the delivered file.
.py notebook. Use the canonical cell layout (one concept per cell, final expression renders, no if guards around outputs, no try/except for control flow)..ipynb to extend or polish: keep it as Jupyter unless the user asks to convert..py notebook:mo.md(r"""...""") or mo.md(f"""...""") only when interpolation is required. Put the prose directly inside the string; never paste quoted string fragments such as " ... " lines inside the markdown body.@app.cell def _(): return placeholders. Remove them before final verification..py file: # /// script
# requires-python = ">=3.12"
# dependencies = [
# "marimo",
# "polars",
# "duckdb",
# "matplotlib",
# # ... add every import used in the notebook
# ]
# ///Run with uv run marimo run <notebook.py> (uv reads the header and resolves the env automatically) or with marimo edit --sandbox <notebook.py> for interactive work.
python3 kernel leaks the system interpreter: pixi run python -m ipykernel install --user --name <project> --display-name "<project> (pixi)"Then in <notebook>.ipynb confirm:
"kernelspec": {"name": "<project>", "display_name": "<project> (pixi)"}Add every import used in the notebook to pixi.toml (or the project's requirements.txt / environment.yml) so the kernel can resolve it from a clean install.
duckdb.read_csv, duckdb.read_parquet). Avoid absolute paths and ~. Avoid hidden state from the runtime working directory.uvx marimo check <notebook.py> before export and fix every reported issue or warning, including empty-cells and markdown formatting. Then run uv run marimo export ipynb <notebook.py> -o <notebook.executed.ipynb> or uv run marimo run <notebook.py> for a non-interactive smoke run; for a deterministic HTML artifact, uv run marimo export html <notebook.py> -o <notebook.html>. Run uvx marimo check <notebook.py> again after the final edit/export cycle.python scripts/execute_notebook.py <notebook.ipynb> (writes <notebook>.executed.ipynb) or pixi run jupyter nbconvert --to notebook --execute --inplace <notebook.ipynb>..ipynb (or in the marimo HTML export).pixi install + jupyter nbconvert --to notebook --execute (Jupyter) must reproduce the same notebook end-to-end..ipynb → marimo .py: uvx marimo convert <notebook.ipynb> -o <notebook.py>, then uvx marimo check <notebook.py>, then clean up Jupyter artifacts (display() calls, %magics, indented final expressions, ipywidget usage). See references/widgets.md and references/latex.md for ipywidget→marimo and MathJax→KaTeX mappings..py → .ipynb: uvx marimo export ipynb <notebook.py> -o <notebook.ipynb>.| Task | Action |
|---|---|
| Author marimo notebook | Edit .py, run uv run marimo edit --sandbox <notebook.py> |
| Author Jupyter notebook | Register pixi kernel, set notebook kernelspec, edit .ipynb |
| Lint marimo notebook | uvx marimo check <notebook.py> |
| Execute marimo headlessly | uv run marimo export ipynb <notebook.py> -o <executed.ipynb> |
| Execute Jupyter headlessly | python scripts/execute_notebook.py <notebook.ipynb> |
Convert .ipynb → marimo | uvx marimo convert <notebook.ipynb> -o <notebook.py> |
Convert marimo → .ipynb | uv run marimo export ipynb <notebook.py> -o <notebook.ipynb> |
| Marimo references | references/notebook_structure.md, references/UI.md, references/SQL.md, references/STATE.md, references/EXPORTS.md, references/PYTEST.md, references/TOP-LEVEL-IMPORTS.md, references/DEPLOYMENT.md |
| Conversion references | references/widgets.md, references/latex.md |
| Pixi + Jupyter | references/pixi_jupyter.md |
| Plot style | references/plot_style.md |
| Templates | templates/marimo_notebook_template.py, templates/jupyter_kiss_template.py |
| Headless executor | scripts/execute_notebook.py |
uv available on PATH, or marimo installed in the environment.pixi available and a pixi.toml (or equivalent env spec) for the project..py for marimo, .ipynb for Jupyter) with narrative markdown cells, code cells, and embedded figures.<notebook>.executed.ipynb or an .html export) where every cell has been run on a fresh kernel..ipynb).kernelspec set to a named pixi kernel.pixi.toml).marimo export ipynb or Jupyter nbconvert --execute exits zero.uvx marimo check <notebook.py> is run by default and reports no issues or warnings; do not treat exit code zero as enough if the output says "Found issues."mo.md(...), trailing empty cells, or return-only placeholder cells remain.# /// script
# requires-python = ">=3.12"
# dependencies = ["marimo", "polars", "duckdb", "matplotlib"]
# ///
import marimo
app = marimo.App(width="medium")
@app.cell
def _():
import marimo as mo
import polars as pl
import duckdb
import matplotlib.pyplot as plt
return mo, pl, duckdb, plt
@app.cell(hide_code=True)
def _(mo):
mo.md(r"""
# Analysis notebook
This notebook loads project data, validates it, and renders the requested figures.
""")
return
@app.cell
def _(duckdb):
df = duckdb.read_parquet("data/measurements.parquet").pl()
df.head()
return (df,)
@app.cell
def _(df, plt):
fig, ax = plt.subplots(figsize=(5, 3.2))
ax.scatter(df["x"], df["y"], s=10)
ax.set_xlabel("x (units)"); ax.set_ylabel("y (units)")
fig
returnThen:
uvx marimo check notebook.py
uv run marimo export ipynb notebook.py -o notebook.executed.ipynb
uvx marimo check notebook.py# One-time kernel registration in the project root:
pixi run python -m ipykernel install --user --name myproject --display-name "myproject (pixi)"
# After authoring, run end-to-end on a fresh kernel:
python skills/notebooks/scripts/execute_notebook.py notebooks/analysis.ipynb \
--kernel myproject \
--out notebooks/analysis.executed.ipynb.ipynb to marimouvx marimo convert notebooks/legacy.ipynb -o notebooks/legacy.py
uvx marimo check notebooks/legacy.py
uv run marimo export ipynb notebooks/legacy.py -o notebooks/legacy.executed.ipynbIssue: Jupyter notebook executes locally but fails on a teammate's machine. Solution: The kernel was unpinned (python3) or used a packaged interpreter outside the project's pixi env. Re-register a named kernel and pin it in the notebook kernelspec.
Issue: Marimo cell does not render a figure. Solution: The figure must be the final expression of the cell. Indented expressions inside if blocks or expressions buried before other statements will not render.
Issue: Figures look correct interactively but the executed file shows empty plots. Solution: Code is mutating shared state across cells (e.g. plt.gcf() reuse). Build a fresh fig, ax = plt.subplots(...) per cell and return / display fig as the final expression.
Issue: Converted notebook fails marimo check. Solution: Remove leftover display(...) calls, drop %magic lines that have no marimo equivalent, and rewrite ipywidget usage using mo.ui.* per references/widgets.md.
Issue: Every matplotlib figure appears twice in the executed Jupyter notebook. Solution: The inline backend's flush_figures post-execute hook auto-displays every open figure (display_data), and the cell's fig return value produces a second copy (execute_result). Unregister the hook in the preamble cell:
plt.ioff()
try:
from matplotlib_inline.backend_inline import flush_figures
get_ipython().events.unregister("post_execute", flush_figures)
except Exception:
passWith this fix, only the cell's final fig expression renders. For figures inside if/else blocks (where fig is not a top-level expression and therefore not captured as execute_result), use display(fig) explicitly instead of bare fig. Do not use mpl.use("agg") as a workaround — it disables _repr_png_() entirely and produces zero images.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.