Financial Model Review Skill
SkillFiles & storageReviews 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.
No other account needed.
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
uvfor script execution.openpyxlrequired for Excel parsing. - Imports (optional):
market-sizing:sizing.json— validate revenue-to-SOM consistencydeck-review:checklist.json— cross-check model-to-deck number alignment
- Exports:
report.json→ic-sim,fundraise-readiness,dd-readinessunit_economics.json→metrics-benchmarker,ic-simrunway.json→fundraise-readiness
Skill Execution Model (READ FIRST)
See
founder-skills/references/skill-execution-model.mdfor 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
Tasktool. The agent does deep analysis, WRITES its output JSON to theOUTPUT_PATHgiven in its prompt (thehandoff/dir), and returns a small receipt. The main thread gates the file withcheck_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 consumeinputs.jsonverbatim, so the main thread pipes the file directly.) - Context B — Post-compose coaching dispatch: The final step dispatches the sub-agent after
compose_report.pywritesreport.md. The sub-agent Reads the stagedcoaching_payload.jsonfrom the hand-off dir (Mitigation 2) — it does NOT read the fullreport.md— composes the coaching commentary, WRITES it to theOUTPUT_PATHhand-off file, and returns a small receipt. The main thread gates the file (check_handoff.py) and inserts it via the sharedinsert_coaching.pyscript (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:
- If the message is wrapped in a
```json ... ```(or plain``` ... ```) fence, strip the fence first. - Try to parse the stripped text directly as JSON.
- If that fails, walk through the text looking for the first
{character and tryjson.JSONDecoder().raw_decode(text[i:])— this is brace-aware and handles nested objects correctly (unlike regex, which truncates on the first}). - 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 filesvalidate_extraction.py— Anti-hallucination gate: cross-referencesmodel_data.jsonagainstinputs.jsonto catch mismatches (company name, salary, revenue, cash traceability); run after extraction, before reviewvalidate_inputs.py— Four-layer validation ofinputs.json(structural, consistency, sanity, completeness); supports--fixto auto-correct sign errorschecklist.py— Scores 46 criteria across 7 categories with profile-based auto-gatingunit_economics.py— Computes and benchmarks 11 unit economics metricsrunway.py— Multi-scenario runway stress-test with decision pointscompose_report.py— Assembles report with cross-artifact validation;--strictexits 1 on high-severity warnings (corrupt/missing artifacts)apply_corrections.py— Processes founder's downloaded corrections file: coerces types, normalizes ILS→USD, merges overrides, writescorrected_inputs.jsonandextraction_corrections.jsonverify_review.py— Review completeness gate: checks artifact existence, content quality, and cross-artifact consistency;--gate 1for after-compose,--gate 2(default) for final; exit 0 = publishable, exit 1 = gaps remainvisualize.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 definitionsschema-inputs.md— JSON schema forinputs.json(the artifact the agent writes)artifact-schemas.md— JSON schemas for script-produced output artifactsdata-sufficiency.md— Data sufficiency gate and qualitative pathextraction-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.
| Step | Artifact | Producer |
|---|---|---|
| 1 | founder context | founder_context.py read/init |
| 2 | model_data.json | extract_model.py (Excel/CSV in main thread) |
| 3 | inputs.json | Context A dispatch: INPUTS_REVIEW → apply_corrections.py |
| 3.5 | corrected_inputs.json | apply_corrections.py (from INPUTS_REVIEW dispatch) |
| 3.6 | extraction_validation.json | validate_extraction.py (when model_data.json exists) |
| 4 | checklist.json | Context A dispatch: CHECKLIST → checklist.py |
| 5 | unit_economics.json | direct pipe: inputs.json → unit_economics.py |
| 6 | runway.json | direct pipe: inputs.json → runway.py |
| 7 | Report | compose_report.py (writes both report.json and report.md) |
| 7.5 | commentary.json | agent-authored (main thread heredoc) — required by Gate 2 for quantitative reviews |
| 8a | HTML report | visualize.py |
| 8b | Explorer | explore.py |
| 8c | Coaching | Context 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.mdfor 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