I’ve been a fan of Squiggle for a while — it’s a language from QURI designed for probabilistic estimation. The to operator alone is worth it: write 5 to 10 and you get a lognormal distribution with a 90% confidence interval between 5 and 10. It makes uncertainty feel natural instead of intimidating.

The problem is that it’s 2026 and I shouldn’t be writing any code - Claude should. So I built a Claude Code skill that writes and executes Squiggle models, then hands back a shareable Playground URL.

What It Does

When I ask something like “estimate how many piano tuners are in New York City,” the skill:

  1. Writes a .squiggle file decomposing the problem into uncertain variables
  2. Runs it against @quri/squiggle-lang via a Node.js runner
  3. Parses the JSON output into a readable summary — mean, median, 90% CI, sparkline
  4. Generates a Squiggle Playground URL so I can view interactive distribution plots and tweak the model

The skill triggers automatically on estimation questions, or manually via /squiggle-fermi.

The Skill Structure

~/.claude/skills/squiggle-fermi/
├── SKILL.md                # Skill definition + style guide
├── reference.md            # Full Squiggle language reference
├── package.json            # Created by setup
├── node_modules/           # @quri/squiggle-lang
└── scripts/
    ├── setup.sh            # One-time npm install
    ├── squiggle-run.mjs    # Executes Squiggle, outputs JSON stats
    └── playground-url.py   # Generates shareable Playground URLs

SKILL.md carries the frontmatter that tells Claude when to activate, plus a condensed style guide for writing good estimates. reference.md holds the full language reference and is loaded on demand so it doesn’t eat context when the skill isn’t active.

The Runner

The interesting piece is squiggle-run.mjs. Squiggle’s npm package (@quri/squiggle-lang v0.10) exposes an async run() function that returns an SqModuleOutput. The runner extracts distribution statistics by reaching into the result chain:

const moduleOutput = await run(code);
const inner = moduleOutput.result.value;

// Named variables live in bindings
for (const [key, val] of inner.bindings.entries()) {
  if (val.tag === "Dist") {
    const dist = val.value; // SqAbstractDistribution
    const mean = dist.mean();
    const samples = dist.getSamples();
    // sort samples, pick percentiles
  }
}

One thing I discovered during setup: the inv() method (for computing quantiles directly) returns undefined in v0.10 despite reporting ok: true. So percentiles are computed from the raw sample array instead — sort the 1,000 samples and index into them. Works fine, just a quirk of the current API.

The output is structured JSON:

{
  "ok": true,
  "bindings": {
    "pianoTuners": {
      "tag": "Dist",
      "summary": {
        "mean": 190,
        "stdev": 144,
        "p5": 50,
        "p50": 153,
        "p95": 435,
        "sparkline": "▁█▅▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁"
      }
    }
  }
}

Playground URLs

After running a model, the skill generates a URL that opens the code directly in the Squiggle Playground. The encoding matches the playground’s native format — JSON payload compressed with zlib, then base64-encoded into the URL fragment:

payload = json.dumps({"defaultCode": code}, separators=(",", ":"))
compressed = zlib.compress(payload.encode("utf-8"), 9)
b64 = base64.b64encode(compressed).decode("utf-8")
url = f"https://www.squiggle-language.com/playground#code={urllib.parse.quote(b64)}"

This means the model is fully self-contained in the URL — no server, no account needed. Anyone with the link sees the interactive model with distribution plots, and can edit it in-place.

The Style Guide Matters

The most important part of the skill isn’t the runner — it’s the style guide baked into SKILL.md. Without it, Claude writes overconfident estimates with narrow ranges. The key rules:

  • a to b is a 90% CI — remind Claude this is very wide, and to still be paranoid about overconfidence
  • Use mixtures for tail riskmx([300 to 400, 50 to 5000], [0.9, 0.1]) adds a fat tail for “what if we’re way off”
  • Decompose, don’t estimate directly — break “total cost” into components so uncertainty compounds realistically
  • Annotate everything@name("Label (units)") and @doc("why this range") on every variable

These come from Squiggle’s own LLM style guide, and they make a noticeable difference in output quality.

Example: Bednet Cost-Effectiveness

Here’s a model the skill produced for malaria bednet distribution:

// Cost-effectiveness: Malaria bednet distribution

@name("Cost per bednet distributed ($)")
costPerNet = 2 to 10

@name("Nets needed per life saved")
@doc("Highly uncertain — depends on region, compliance, existing coverage.
Wide mixture to account for possibility we're very wrong.")
netsPerLifeSaved = mx([300 to 800, 100 to 5000], [0.8, 0.2])

@name("Cost per life saved ($)")
@format("$,.0f")
costPerLifeSaved = costPerNet * netsPerLifeSaved

@name("Value of a statistical life ($)")
@doc("Using GiveWell-style moral weights, not US VSL")
valuePerLife = 50k to 500k

@name("Benefit-cost ratio")
benefitCostRatio = valuePerLife / costPerLifeSaved

Results:

Cost per life saved
  Mean:   ~$3,400
  Median: ~$2,400
  90% CI: $780 to $8,800

Benefit-cost ratio
  Mean:   ~114x
  Median: ~68x
  90% CI: 13x to 357x

The mixture on netsPerLifeSaved is doing important work — the 20% weight on the wide component (100 to 5,000) means the model doesn’t collapse into false precision even when the central estimate is relatively tight.

Setup

One-time install:

bash ~/.claude/skills/squiggle-fermi/scripts/setup.sh

This runs npm install @quri/squiggle-lang inside the skill directory. After that, the skill is available in every Claude Code session — no per-project setup needed.

What I’d Like to Improve

  • Context7 integration — the skill could pull live Squiggle documentation via the Context7 MCP server instead of maintaining a static reference. The docs evolve and the skill’s reference will drift over time.
  • Better sensitivity analysis — it would be useful to automatically identify which input variable contributes most to output variance, so you know where to spend effort refining your estimates.
  • Squiggle Hub integration — beyond playground URLs, it could publish models directly to squigglehub.org for persistent sharing and collaboration.