Financial Model Review Skill

SkillFiles & storage

Reviews startup financial models for investor readiness — validates unit economics, stress-tests runway scenarios, and benchmarks metrics against stage-appropriate targets. Accepts Excel, CSV, or text. Run the source-cited stage benchmarks rather than recalling them. Also covers plain-language money questions with no file attached — 'how long do I have?', 'when do I run out of cash?', 'is a 4x burn multiple bad?' — which run the real calculator instead of mental arithmetic.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Financial Model Review Skill skill

What this skill tells your AI

The instructions your AI receives, as published by lool-ventures/founder-skills in founder-skills/skills/financial-model-review/SKILL.md and read by ahel’s review.

Help startup founders understand how investors will evaluate their financial model — validating structure, unit economics, runway, and metrics against stage-appropriate standards. Produce a thorough review with actionable improvements. The tone is founder-first: a rigorous but supportive coaching session.

Skill Metadata

  • Author: lool-ventures
  • Version: managed in founder-skills/.claude-plugin/plugin.json
  • Compatibility: Python 3.10+ and uv for script execution. openpyxl required for Excel parsing.
  • Imports (optional):
    • market-sizing:sizing.json — validate revenue-to-SOM consistency
    • deck-review:checklist.json — cross-check model-to-deck number alignment
  • Exports:
    • report.jsonic-sim, fundraise-readiness, dd-readiness
    • unit_economics.jsonmetrics-benchmarker, ic-sim
    • runway.jsonfundraise-readiness

Skill Execution Model (READ FIRST)

See founder-skills/references/skill-execution-model.md for the full inline-skill execution model (3 dispatch contexts, Mitigation 1+2, producer contract, Cowork quirks, per-symptom triage).

This skill runs inline in the main thread, not as a sub-agent — see the reference above ("Why Inline (Not Forked Sub-Agent)") for the rationale. Sub-agents are deliberately shell-free, so orchestration (producer scripts, artifact persistence) stays in the main thread.

Two dispatch contexts for the sub-agent:

  • Context A — Per-step analytical dispatch (Mitigation 1): The INPUTS_REVIEW and CHECKLIST steps dispatch the financial-model-review agent via the Task tool. The agent does deep analysis, WRITES its output JSON to the OUTPUT_PATH given in its prompt (the handoff/ dir), and returns a small receipt. The main thread gates the file with check_handoff.py, then pipes it through the producer script. The sub-agent never writes canonical artifacts — only its hand-off file. (Unit economics and runway are NOT dispatched — those producers consume inputs.json verbatim, so the main thread pipes the file directly.)
  • Context B — Post-compose coaching dispatch: The final step dispatches the sub-agent after compose_report.py writes report.md. The sub-agent Reads the staged coaching_payload.json from the hand-off dir (Mitigation 2) — it does NOT read the full report.md — composes the coaching commentary, WRITES it to the OUTPUT_PATH hand-off file, and returns a small receipt. The main thread gates the file (check_handoff.py) and inserts it via the shared insert_coaching.py script (idempotency matrix, uuid-marker replacement, run_id-parity verification — all deterministic). See the reference above for the full Context B contract.

Tolerant JSON extraction protocol (Context B returns; also the Context A message-channel fallback): capture the sub-agent's final assistant message. It should be raw JSON, but may be wrapped in ```json ... ``` fences or carry a prose preamble. Extract tolerantly:

  1. If the message is wrapped in a ```json ... ``` (or plain ``` ... ```) fence, strip the fence first.
  2. Try to parse the stripped text directly as JSON.
  3. If that fails, walk through the text looking for the first { character and try json.JSONDecoder().raw_decode(text[i:]) — this is brace-aware and handles nested objects correctly (unlike regex, which truncates on the first }).
  4. If extraction fails entirely, re-prompt the sub-agent with: "Your previous reply could not be parsed as JSON. Return ONLY the JSON object — no markdown fences, no prose preamble."

Context A receipts don't need this protocol by hand — check_handoff.py --receipt-json - applies the same tolerant extraction internally; pass the final message verbatim.

If a sub-agent wrote CANONICAL artifact files directly anyway (anything outside its handoff/ OUTPUT_PATH): do not trust them — take its gated hand-off file (or extract the JSON from its final message on the fallback path), then re-pipe through the producer script as specified; the producer overwrites the file with the validated, run_id-stamped version. For INPUTS_REVIEW specifically: if inputs.json contains the {"corrected": ..., "corrections": ...} wrapper, the sub-agent wrote its reply to disk — feed that wrapper through apply_corrections.py as usual.

Context-pressure note: This skill has the highest context budget of the 5 skills. The win from Mitigation 1 is excluding sub-agent reasoning and the raw extract_model.py output (which can run to megabytes on real models) — which flows through the INPUTS_REVIEW dispatch: the sub-agent reads it in its own context window, returns only the corrected inputs.json. The artifacts themselves still accumulate in the main thread (~80-130K total), but that is manageable.

Input Formats

Accept any format: Excel (.xlsx), CSV, Google Sheets exports, financial documents, or conversational input. For Excel files, use extract_model.py to parse. For other formats, extract data manually into the inputs.json schema. If multiple copies of the same file exist (e.g., Financials.xlsx and Financials (1).xlsx), use the most recently modified version and note the duplication to the founder. If timestamps are identical, ask the founder which file to use. If the founder cannot be queried, prefer the file without parenthetical suffixes (e.g., (1), (2)) — these typically indicate browser re-download duplicates.

Available Scripts

All scripts are at ${CLAUDE_PLUGIN_ROOT}/skills/financial-model-review/scripts/:

  • extract_model.py — Extracts structured data from Excel (.xlsx) and CSV files
  • validate_extraction.py — Anti-hallucination gate: cross-references model_data.json against inputs.json to catch mismatches (company name, salary, revenue, cash traceability); run after extraction, before review
  • validate_inputs.py — Four-layer validation of inputs.json (structural, consistency, sanity, completeness); supports --fix to auto-correct sign errors
  • checklist.py — Scores 46 criteria across 7 categories with profile-based auto-gating
  • unit_economics.py — Computes and benchmarks 11 unit economics metrics
  • runway.py — Multi-scenario runway stress-test with decision points
  • compose_report.py — Assembles report with cross-artifact validation; --strict exits 1 on high-severity warnings (corrupt/missing artifacts)
  • apply_corrections.py — Processes founder's downloaded corrections file: coerces types, normalizes ILS→USD, merges overrides, writes corrected_inputs.json and extraction_corrections.json
  • verify_review.py — Review completeness gate: checks artifact existence, content quality, and cross-artifact consistency; --gate 1 for after-compose, --gate 2 (default) for final; exit 0 = publishable, exit 1 = gaps remain
  • visualize.py — Generates self-contained HTML with SVG charts (not JSON)
  • explore.py — Generates self-contained interactive HTML explorer from review artifacts; outputs HTML (not JSON)
  • review_inputs.py — Dual-mode review viewer: HTTP server with live validation (Claude Code) or self-contained static HTML with JS sanity metrics (Cowork); outputs HTML

Also available from ${CLAUDE_PLUGIN_ROOT}/scripts/ (shared):

  • find_artifact.py — Resolves artifact paths by skill name and filename (used for cross-skill lookups)

Run with: python3 ${CLAUDE_PLUGIN_ROOT}/skills/financial-model-review/scripts/<script>.py --pretty [args]

Available References

Read as needed from ${CLAUDE_PLUGIN_ROOT}/skills/financial-model-review/references/:

  • checklist-criteria.md — All 46 checklist criteria with gate definitions
  • schema-inputs.md — JSON schema for inputs.json (the artifact the agent writes)
  • artifact-schemas.md — JSON schemas for script-produced output artifacts
  • data-sufficiency.md — Data sufficiency gate and qualitative path
  • extraction-pitfalls.md — 8 common extraction errors (scale denomination, payroll aggregation, collections vs revenue, etc.)

From ${CLAUDE_PLUGIN_ROOT}/references/ (shared): stage-expectations.md, benchmarks.md, israel-guidance.md, revenue-model-types.md, common-mistakes.md

Artifact Pipeline

Every review deposits structured JSON artifacts into a working directory. The final step assembles all artifacts into a report and validates consistency. This is not optional.

StepArtifactProducer
1founder contextfounder_context.py read/init
2model_data.jsonextract_model.py (Excel/CSV in main thread)
3inputs.jsonContext A dispatch: INPUTS_REVIEW → apply_corrections.py
3.5corrected_inputs.jsonapply_corrections.py (from INPUTS_REVIEW dispatch)
3.6extraction_validation.jsonvalidate_extraction.py (when model_data.json exists)
4checklist.jsonContext A dispatch: CHECKLIST → checklist.py
5unit_economics.jsondirect pipe: inputs.jsonunit_economics.py
6runway.jsondirect pipe: inputs.jsonrunway.py
7Reportcompose_report.py (writes both report.json and report.md)
7.5commentary.jsonagent-authored (main thread heredoc) — required by Gate 2 for quantitative reviews
8aHTML reportvisualize.py
8bExplorerexplore.py
8cCoachingContext B dispatch: POST_COMPOSE_COACHING

Rules:

  • Deposit each artifact before proceeding to the next step
  • For agent-written artifacts (inputs.json), consult references/schema-inputs.md for the JSON schema
  • If a step is not applicable, deposit a stub: {"skipped": true, "reason": "..."}
  • Do NOT use isolation: "worktree" for sub-agents — files written in a worktree won't appear in the main $REVIEW_DIR

Keep the founder informed with brief, plain-language updates at each step. Narrate the founder-visible OUTCOME, never the internal step. That is the test to apply, and it catches more than a word list can: the forbidden thing is not a syntax, it is talking about the machinery. Bad — "Gating and piping the extraction through the producer, then staging the coaching hand-off"; good — "I've checked your numbers and I'm writing up what stood out." Bad — "schema-drift warning on coaching_payload"; good — nothing, because the founder has no stake in it. Never name an internal artifact, field, or token (a payload key, a marker name, an artifact filename, a hand-off dir) even in plain prose with no backticks — a detector keyed on syntax cannot see "gated", "hand-off" or "canonical artifacts", but the founder still reads them and they still mean nothing to them. The between-step progress lines are the primary leak vector, not the final summary. They feel internal — you are narrating what you are about to do — but the founder reads every one of them, and this is where the leaks actually appear: "Now gating the hand-off before piping through the checklist producer", "Gate 1 passes", "Running the final verification gate". Rewrite each pipeline transition as the founder-visible outcome: "Checking your numbers against the 46-point review", "Your inputs look consistent — moving on to unit economics", "Finishing up and putting the report together". If a progress line would mean nothing to someone who has never seen this skill's internals, it does not belong in the channel. Also excluded, as before: file/script names, paths, *.py, --flags, $vars, exit codes ("Exit N", "not found"), W_/E_ codes, JSON, and step/route labels ("Lane N", "Context A/B", "Phase N", "structure detection", "the grid", any ALL_CAPS_TOKEN). After each analytical step (3–6), share a one-sentence finding before moving on. Track progress with at most one batched task tracker (a single TaskCreate), updating it only at phase boundaries — extraction, review gate, scoring, report — never per sub-step: the step narration above is the founder's progress channel, so per-substep TaskCreate/update churn only adds runtime. The task tracker is founder-visible too — the same rule governs its labels. "Gate the inputs review handoff", "Validate inputs.json", "resolve agent namespace paths", "Initialize founder context" are leaks even though each names a real step, and even when the prose around them is clean. Label each task by the founder-visible outcome — "Check your inputs", "Score against the review", "Write up what I found" — never by a file, directory, script, or pipeline stage.

Workflow

Step 0: Path Setup

Every Bash tool call runs in a fresh shell — variables do not persist. Run the block below exactly once: it resolves $PLUGIN_ROOT deterministically, and every later block must substitute the printed value as a literal rather than re-running the resolution — repeating the self-heal search can land on a different mount than Step 0 picked when more than one is present (see why in the block's comments).

Optional, best-effort, and via the Read tool (not a shell command): before the block below, Read ${CLAUDE_PLUGIN_ROOT}/.claude-plugin/plugin.json and note its version field as EXPECT_VERSION. Passing it to select_plugin_root.py below lets an exact version match win over an arbitrary first hit. If the Read fails, skip it and omit --expect-version — selection is still deterministic without it.

SCRIPTS="${CLAUDE_PLUGIN_ROOT}/skills/financial-model-review/scripts"
if [ ! -d "$SCRIPTS" ]; then
  # In Cowork, CLAUDE_PLUGIN_ROOT substitutes to a host-side path absent inside
  # the session VM — self-heal by collecting EVERY candidate mount (a session can
  # have more than one at once: a stale host-side cache, a test marketplace, even
  # a symlink into a different session's tree) and handing them to
  # select_plugin_root.py, which picks ONE deterministically and names the
  # rejects — never trust `find`'s arbitrary first hit, which can silently mix
  # scripts across plugin versions mid-pipeline.
  CANDIDATES="$(find /sessions -type d -path '*/skills/financial-model-review/scripts' 2>/dev/null)"
  [ -n "$CANDIDATES" ] || CANDIDATES="$(find / -type d -path '*/skills/financial-model-review/scripts' 2>/dev/null)"
  PROVISIONAL_ROOT="$(printf '%s\n' "$CANDIDATES" | head -1)"
  PROVISIONAL_ROOT="${PROVISIONAL_ROOT%/skills/*}"
  # Bootstrap order: $SHARED_SCRIPTS isn't known until a root is chosen, so use the
  # provisional root's OWN copy of the selector; an older plugin copy without one
  # falls back to the provisional root unchanged.
  SELECTOR="$PROVISIONAL_ROOT/scripts/select_plugin_root.py"
  if [ -f "$SELECTOR" ]; then
    if [ -n "$EXPECT_VERSION" ]; then
      PLUGIN_ROOT="$(printf '%s\n' "$CANDIDATES" | python3 "$SELECTOR" --expect-version "$EXPECT_VERSION")"
    else
      PLUGIN_ROOT="$(printf '%s\n' "$CANDIDATES" | python3 "$SELECTOR")"
    fi
  else
    PLUGIN_ROOT="$PROVISIONAL_ROOT"
  fi
  SCRIPTS="$PLUGIN_ROOT/skills/financial-model-review/scripts"
fi
PLUGIN_ROOT="${SCRIPTS%/skills/*}"
echo "PLUGIN_ROOT=$PLUGIN_ROOT"   # resolved ONCE, here — paste this literal into every later block; never re-run this resolution
REFS="$PLUGIN_ROOT/skills/financial-model-review/references"
SHARED_SCRIPTS="$PLUGIN_ROOT/scripts"
SHARED_REFS="$PLUGIN_ROOT/references"
# Resolve the canonical artifacts root via a SCRIPT, not inline bash (the agent paraphrases inline
# path computations → outputs/ vs outputs/artifacts/ drift across runs). Deterministic + creates it.
python3 "$SHARED_SCRIPTS/resolve_artifacts_root.py"   # prints ARTIFACTS_ROOT — use the printed path verbatim as ARTIFACTS_ROOT in every later block (a captured var dies in the next fresh shell)

Reaching the self-heal branch is normal in Cowork — ${CLAUDE_PLUGIN_ROOT} resolves to a HOST path that does not exist inside the VM, so the [ ! -d "$SCRIPTS" ] test fails by design rather than by misconfiguration. It is not a sign anything is wrong, and it is not worth narrating to the founder.

Outputs mount is append-only. Everything under the promoted outputs mount (.../mnt/outputs/, not just $REVIEW_DIR) is write-allowed and delete-denied by the platform: never rm, move away, or empty anything under it — including files you created yourself. Never create ad-hoc scratch anywhere under the outputs mount (no _src/ copies, no run-state note files); scratch belongs in $STAGING_DIR (a /tmp dir, defined below). Do not "clean up" the outputs folder before delivering — extra working files there are expected and harmless. The uploaded document is already readable in place from the uploads mount; never copy it under outputs to make it readable.

If ARTIFACTS_ROOT resolves to $(pwd)/artifacts but no artifacts/ directory exists at $(pwd): The workspace may not be mounted yet. Use Glob with pattern **/artifacts/founder_context.json to locate existing artifacts, and derive ARTIFACTS_ROOT from the result. If nothing is found, mkdir -p "$ARTIFACTS_ROOT" and proceed — never a relative ./artifacts, which resolves against the shell's cwd (the session root) and lands outside the outputs mount, undelivered.

After Step 1 (when the slug is known), derive REVIEW_DIR. Two modes — pick exactly one:

  • Full review (default — the founder attached a model, asked for a review, a report, or the interactive explorer, OR there is no existing full review for this slug): run Steps 2–11. REVIEW_DIR="$ARTIFACTS_ROOT/financial-model-review-${SLUG}".
  • Quick-check mode — a single directional question in conversation, no model attached and no request for a review ("with $400k in the bank and $60k/mo net burn, how long do I have?", "is a 4x burn multiple bad at seed?"). Run Step 5-quick instead of Steps 2–11. REVIEW_DIR="$ARTIFACTS_ROOT/financial-model-review-${SLUG}-quickcheck".

Tie-breaker when both bullets seem to fit. Decide on the verb, not the inputs: a request for the work product ("review my model, analyze our runway, I need this for the board") is a full run even when every number is already in hand, while a request for a read ("roughly, ballpark, how long do I have, is X bad") is a quick check even when materials are attached. Complete inputs make the full run faster, not less wanted. When the verb is genuinely absent, default to the full run and say you did — an unwanted full run costs time, an unwanted quick check costs the founder the analysis they came for.

Never answer from your own arithmetic. Quick-check exists because the alternative a model reaches for — computing runway in its head and offering the real review as an opt-in — produces a number with no scenario stress-test, no benchmark provenance, and no record, under this skill's name. Running fewer producers is fine; running none is not.

Step 5-quick: the quick-check path

Run only the producer(s) the question actually needs, with the inputs the founder gave you:

# Runway question -> runway.py alone. Unit-economics question -> unit_economics.py alone.
printf '%s' "$QUICK_JSON" | python3 "$SCRIPTS/runway.py" --stdin --pretty \
  --run-id "$RUN_ID" -o "$REVIEW_DIR/runway.json"

Producers deliberately NOT run: extract_model.py, validate_extraction.py, validate_inputs.py, checklist.py, the producer the question didn't need, compose_report.py, visualize.py, explore.py, verify_review.py, and the Context-B coaching dispatch. No report.md is written.

Same-numbers guarantee. The figures are identical to what the full review would compute from the same inputs — it is the same script reading the same shape. Only the production weight is dropped. What you do not get is what the skipped producers add: the anti-hallucination extraction gate, the four-layer input validation, the 46-item checklist, multi-scenario stress-testing, and the cross-artifact consistency checks.

Presenting it. Label it a quick check, not a review. Give the figure, name the inputs it came from, and state plainly that nothing was validated or stress-tested. Then close with a statement, never a question: "The full review validates the model, stress-tests runway across scenarios, and scores 46 investor criteria — say the word and I'll run it." A question invites a "no" to something the founder would have wanted.

REVIEW_DIR="${REVIEW_DIR:-$ARTIFACTS_ROOT/financial-model-review-${SLUG}}"              # full review
# REVIEW_DIR="${REVIEW_DIR:-$ARTIFACTS_ROOT/financial-model-review-${SLUG}-quickcheck}"  # quick check
mkdir -p "$REVIEW_DIR"
RUN_ID="$(date -u +%Y%m%dT%H%M%SZ)"
# Context A hand-off dir — PER RUN: sub-agents WRITE their raw output JSON here (the audit trail —
# raw sub-agent output as returned, before producer validation). Permanent by platform design
# (outputs/ mounts are write-allowed / delete-denied); nothing in it is ever a canonical artifact.
# The $RUN_ID segment is load-bearing: it prevents a stale prior-run file from silently passing
# the hand-off gate when a dispatch fails to write.
HANDOFF_DIR="$REVIEW_DIR/handoff/$RUN_ID"
mkdir -p "$HANDOFF_DIR"
# Sub-agents address the SAME dir by a different path (their file tools are rooted at the outputs
# mount in Cowork). Resolve the FULL agent-namespace paths via the script — never hand-splice the
# printed root with a literal skill-name/slug/run-id string yourself (that string-splicing is
# exactly the non-determinism the resolver script exists to remove):
python3 "$SHARED_SCRIPTS/resolve_artifacts_root.py" --handoff-dir-agent \
  --dir-name "financial-model-review-${SLUG}" --run-id "$RUN_ID"   # prints HANDOFF_AGENT verbatim
HANDOFF_AGENT="<printed value>"   # use verbatim in OUTPUT_PATH lines
# Sub-agent READ paths for under-outputs artifacts use the SAME agent namespace (relative — the
# sub-agent's file-tool cwd IS the outputs mount on host-loop; an absolute /sessions/... read is denied):
python3 "$SHARED_SCRIPTS/resolve_artifacts_root.py" --analysis-dir-agent \
  --dir-name "financial-model-review-${SLUG}"   # prints the dir in the agent namespace
REVIEW_DIR_AGENT="<printed value>"   # e.g. model_data.json, inputs.json reads
# Ad-hoc scratch (NOT sub-agent hand-off) lives OUTSIDE the promoted outputs/ tree, in a temp dir
# that is safe to both create and reclaim. Use the printed path verbatim in later steps.
STAGING_DIR="$(mktemp -d "${TMPDIR:-/tmp}/financial-model-review-${SLUG:-fmr}.staging.XXXXXX")"

Pass RUN_ID to all sub-agents. The four producer artifacts (inputs.json, checklist.json, unit_economics.json, runway.json) must carry "metadata": {"run_id": "$RUN_ID"} at the top level — including skipped stubs, whose stub heredoc carries the same "metadata": {"run_id": "$RUN_ID"} block. The producers propagate it from their stdin payloads; never hand-edit script outputs to add it. (model_data.json and extraction_validation.json have no run_id by design.) compose_report.py checks that all present run IDs match — a mismatch triggers a STALE_ARTIFACT high-severity warning, blocking under --strict. Stub artifacts are exempt from the value comparison but still carry the run_id key so the Context B parity grep finds it.

Overwrite-in-place — do NOT delete prior artifacts under $REVIEW_DIR. It is the promoted outputs/ tree in Cowork, where deleting a user-visible path is unsafe (Cowork can deny it; the parity gate flags it). Each producer writes its artifact fresh via -o every run, and RUN_ID is minted fresh per run — so if a prior run left an artifact a later step doesn't regenerate, compose_report.py's STALE_ARTIFACT check (run_ids must match) catches the mismatch. No bulk rm is needed or wanted.

Step 1: Read or Create Founder Context

python3 "$SHARED_SCRIPTS/founder_context.py" read --artifacts-root "$ARTIFACTS_ROOT" --pretty

Three cases based on exit code:

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
34
Forks
3
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
financial-model-review
Source
github.com/lool-ventures/founder-skills