| name | write-path-mapping |
| description | Map the write path of a project across multiple frameworks — entry points, validation, auth, persistence, side-effects. Outputs report, Mermaid diagrams, JSON sidecar. Flags unauth writes, missing RLS, cache gaps. Use for write path, mutation audit, RLS audit. |
| argument-hint | ["target-directory-or-package"] |
| allowed-tools | Read Grep Glob Write Edit Bash(git:*, ls:*, wc:*, find:*, cat:*, mkdir:*, test:*) Agent |
| effort | high |
Write Path Mapping
ultrathink
Output path directive (canonical — overrides in-body references).
All file outputs from this skill MUST be written under .anthril/reports/write-path-mapping/.
Run mkdir -p .anthril/reports/write-path-mapping before the first Write call.
Primary artefact: .anthril/reports/write-path-mapping/<artefact>.
Do NOT write to the project root or to bare filenames at cwd.
Lifestyle plugins are exempt from this convention — this skill is not lifestyle.
Before You Start
- Locate the target. Use
$ARGUMENTS if provided, otherwise the current working directory. If neither resolves to a real directory, ask the user for the target path before continuing.
- Detect the stack. Run
scripts/detect-stack.sh to identify languages, frameworks, and monorepo layout. This determines which framework matrix to apply in Phase 2.
- Detect the persistence layer. Run
scripts/detect-db.sh to identify Supabase, Prisma, Drizzle, TypeORM, SQLAlchemy, Django ORM, ActiveRecord, Eloquent, Doctrine, Redis, etc. This determines which mutation matrix to apply in Phase 5.
- Check tooling availability. Run
scripts/check-tools.sh. Missing tools degrade depth but never abort the mapping — ripgrep is the only strongly recommended tool.
- Load
.write-path-ignore. If the target contains this file, parse it and treat entries as suppression rules during Phase 8.
- Map project structure. Inventory the codebase excluding
node_modules/, .venv/, venv/, target/, dist/, build/, .next/, .nuxt/, coverage/, .git/, .turbo/.
User Context
$ARGUMENTS
Detected stack: !bash "${CLAUDE_PLUGIN_ROOT}/skills/write-path-mapping/scripts/detect-stack.sh" .
Detected persistence: !bash "${CLAUDE_PLUGIN_ROOT}/skills/write-path-mapping/scripts/detect-db.sh" .
Tool availability: !bash "${CLAUDE_PLUGIN_ROOT}/skills/write-path-mapping/scripts/check-tools.sh" .
Candidate write entries (fast ripgrep seed): !bash "${CLAUDE_PLUGIN_ROOT}/skills/write-path-mapping/scripts/find-write-endpoints.sh" .
Mapping Phases
Execute every phase in order. For each path, record: entry (type, file, line, verb, route, framework), middleware chain, validator, auth layer, handler, persistence targets, fan-out count, downstream effects, risks, verification depth. Use the rubric in ${CLAUDE_PLUGIN_ROOT}/skills/write-path-mapping/reference.md §7 for completeness scoring. Never skip a phase — mark as N/A if genuinely not applicable to the detected stack.
Read-only guarantee. This skill never modifies source files. It emits three new artifacts into the target project:
write-path-map.md — the main report
write-path-map.json — the JSON sidecar
risk-register.md — the standalone risk register
Completeness, not quality. The completeness score measures how thoroughly the skill traced the system, NOT how good the system is. A clean codebase and a messy codebase can both score 100%. System quality is captured separately in the Risk Register.
Phase 1: Discovery & Inventory (context only — no score)
Objective: Build an accurate picture of the codebase and its persistence layer before any mapping begins.
- Read top-level config:
package.json, pyproject.toml / setup.py / requirements*.txt, go.mod, Cargo.toml, pom.xml / build.gradle*, composer.json, Gemfile, *.csproj / *.sln.
- Detect monorepo layout:
pnpm-workspace.yaml, lerna.json, nx.json, turbo.json, Cargo.toml [workspace], go.work, rush.json. Each package becomes a mapping target.
- Run
scripts/extract-schema.sh to collect tables, columns, and schema file inventory.
- Run
scripts/extract-triggers.sh to collect CREATE TRIGGER, CREATE FUNCTION, and CREATE POLICY statements.
- Run
scripts/extract-cron.sh to collect scheduled job sources (BullMQ repeat, pg_cron, Celery beat, Rails whenever, GitHub Actions schedule, Vercel Cron, etc.).
- Run
scripts/extract-queues.sh to collect queue producers and consumers (BullMQ, SQS, Kafka, NATS, Celery, Sidekiq, Supabase queue, Cloudflare Queues).
- Live DB enrichment (optional): run
scripts/live-db-probe.sh. If Supabase MCP is configured or DATABASE_URL is set, enrich the schema data with live pg_policies, pg_trigger, and information_schema queries. When Supabase MCP is available, prefer the mcp__*Supabase__execute_sql (SELECT only) and mcp__*Supabase__list_tables tools over psql. If no live source is available, continue with static data.
- Load
.write-path-ignore if present. Surface unjustified entries and stale patterns as Phase 1 warnings.
This phase produces the context block at the top of the report. No completeness score of its own — phase 1 failures become adjustments to overall completeness.
Phase 2: Entry-Point Discovery
Objective: Find every write entry point in the project.
- Consume the seed list from
scripts/find-write-endpoints.sh (already run in User Context).
- Optionally run
python3 scripts/ast-entrypoints.py for richer AST-level extraction. Its output is merged with the seed list via scripts/normalize-findings.py.
- Classify every candidate using the taxonomy in
reference.md §1.
- MANDATORY sub-agent sweep when >30 candidates. Partition the seed list by top-level domain folder (e.g.
app/, src/api/, supabase/functions/, workers/, each monorepo package) and spawn one Agent(subagent_type=Explore) per domain in parallel (multiple tool calls in one assistant message). Each sub-agent receives the prompt shape from reference.md §9 and returns a JSON array. Merge via scripts/normalize-findings.py.
- Deduplicate by
(file, line, verb).
- Drop entries that are clearly read-only (GET routes, pure-query GraphQL queries, reader RPCs).
Phase completeness = traced_entries / discovered_candidates × 100.
Phase 3: Middleware & Auth Layer Capture
Objective: For every entry, capture the middleware chain and the auth layer.
- For each entry, walk the router registration back to the framework bootstrap and record every
use(), decorator, guard, before_action, Depends(...), middleware alias, or firewall entry. Use the matrix in reference.md §5.
- Resolve framework-specific auth: Next.js
middleware.ts, NestJS @UseGuards(AuthGuard), FastAPI Depends(get_current_user), Django @login_required / permission classes, Rails before_action :authenticate_user!, Laravel ->middleware('auth'), Symfony firewalls.
- Supabase RLS cross-reference. For Supabase projects, run
scripts/rls-policy-check.sh and intersect each write target table with the RLS policies discovered in Phase 1. Tables with no policy covering the relevant role become missing-rls candidates.
- Tentatively flag risks (final resolution in Phase 7):
- Entry with no resolvable auth layer →
unauth-write
- Webhook entry with no signature verification →
unverified-webhook
- Edge function using
SUPABASE_SERVICE_ROLE_KEY to write on behalf of user input → service-role-overreach
- Record the middleware chain in each path's
middleware array.
Phase completeness = entries_with_auth_layer_recorded / entries × 100.
Phase 4: Validator Detection
Objective: Confirm each entry validates its input.
- Detect the project's validator libraries (Zod, Yup, Joi, class-validator, Valibot, ArkType, Pydantic, DRF serializers, Go struct tags, PHP voters, Rails strong parameters).
- For each entry, walk the first N lines of the handler for a
.parse(...), .validate(...), @Body(new ValidationPipe()), class SomeDto, schema=ModelSerializer, request.validate(...), or struct-binding pattern.
- Record the validator in
path.validator = { lib, schema, file, line }. If none is found, tentatively flag missing-validation (confirmed in Phase 7).
- Where possible, cross-reference validator fields against target table columns to detect fields accepted without validation.
Phase completeness = entries_with_validator_recorded_or_confirmed_none / entries × 100.
Phase 5: Handler & Persistence Trace
Objective: For every entry, walk the handler into every persistence target.
-
For each handler, resolve imports transitively and enumerate every call matching the persistence matrix in reference.md §4. Start with python3 scripts/ast-write-calls.py on the handler file (and the files it imports). Record each target: kind, target, file, line.
-
Run python3 scripts/transaction-boundary-check.py on the handler file(s) and merge in_transaction flags into the target records.
-
MANDATORY sub-agent sweep when any of:
- Project has >30 entries total.
- Any single handler reaches 3+ service-layer hops (e.g.
route → service → repo → util).
- The stack is polyglot (JS frontend + Python worker + Go gateway).
Partition handlers into batches of 10 and spawn parallel Agent(subagent_type=Explore) calls. Each sub-agent receives the prompt shape from reference.md §9 Phase 5 and returns JSON per handler matching the path block of templates/paths-schema.json.
-
Merge all outputs via scripts/normalize-findings.py.
-
Count fan_out_count = len(persistence_targets) for every path.
-
Paths with zero targets are reclassified as read-only and dropped from the write map (surfaced in §11 Suppressed Paths).
Phase completeness = entries_with_at_least_one_persistence_target / entries × 100. Depth = % of handlers where every delegate was resolved vs. stopped at dynamic dispatch.
Phase 6: Async / Fan-Out / DB-Side Effects
Objective: Map non-obvious writes that run after the initial handler returns.
- Queue fan-out. For every
queue-publish found in Phase 5, locate the consumer. If not found in the immediate directory, spawn one sub-agent per unresolved queue with the Phase 6 playbook prompt in reference.md §9. Cross-package search includes ../cloudflare-workers/, ../workers/, docker-compose services, and supabase/functions/. Record consumers as secondary entry points and run Phases 2–5 on them.
- Cron / scheduled jobs. Using the
extract-cron.sh output from Phase 1, treat every scheduled job as a secondary entry point and run Phases 2–5 on its handler.
- DB trigger chains. For every table touched in Phase 5, look up triggers from the
extract-triggers.sh output. For each trigger, record its target table(s) as downstream_effects of kind db-trigger. If there are >5 triggers on the project's schema, spawn one sub-agent to walk the full trigger graph.
- Domain events. For every
event-emit found in Phase 5, locate the subscribers. Record as downstream_effects of kind event-subscriber.
- Supabase realtime broadcasts. For every
channel.send({type: 'broadcast'}) found, note the channel as a downstream effect.
- Orphan detection. Queues with producers but no consumer →
orphan-queue-consumer (HIGH). Consumers with no producer → orphan-queue-publish (LOW). Triggers referencing functions that no longer exist → dead-trigger (HIGH).
Phase completeness = resolved_async_targets / discovered_async_targets × 100.
Phase 7: Risk Analysis
Objective: Walk every mapped path through the risk taxonomy and record evidence.
For each path, check every risk subtype from reference.md §6. Record evidence per flagged risk. Apply context-aware severity adjustments:
unauth-write on a path protected by RLS → downgrade to HIGH. Note the RLS mitigation in evidence.
missing-transaction where only one persistence target exists → suppress.
missing-rls confirmed via rls-policy-check.sh → CRITICAL.
cross-tenant-leak detected when a Supabase/ORM write omits workspace_id (or equivalent) on a workspace-scoped table → CRITICAL.
dynamic-dispatch-write → always INFO. The skill cannot evaluate dynamic dispatch; the human reviewer must.
fan-out-write with ≥3 targets but all inside a transaction → INFO. Without a transaction → HIGH.
Deep-dive sub-agents (optional). For any CRITICAL-flagged path with ≥3 middleware layers, spawn one Agent(subagent_type=Explore) to deep-verify the finding before publishing it. The sub-agent confirms whether a compensating control (RLS policy, signature verification, rate limit, tenancy filter) exists elsewhere in the chain and returns {confirmed, evidence, severity_adjustment}.
Phase completeness = paths_with_all_risks_checked / paths × 100.
Phase 8: Reporting
Objective: Produce the final artifact set.
- Merge all findings. Run
python3 scripts/normalize-findings.py one final time to produce the unified write-path-map.json under the target project root.
- Render the main report. Use
templates/output-template.md as the structural skeleton. The report must include:
- Header table (date, stack, persistence, totals, risks by severity, completeness, tier)
- §1 Executive summary (top paths by fan-out, top risks, top data-domain hotspots)
- §2 Stack & persistence
- §3 Write paths by severity (CRITICAL/HIGH/MEDIUM/INFO/OK)
- §4 Write paths by domain
- §5 Per-endpoint detail blocks (top 20, ordered severity then fan-out)
- §6 Data-domain write map (tables, caches, queues, external APIs, file stores)
- §7 Risk register (inline copy)
- §8 Suggested
.write-path-ignore entries
- §9 Visual artifacts (all four Mermaid diagrams)
- §10 JSON sidecar pointer with example
- §11 Suppressed paths
- Render the Mermaid diagrams. Run
python3 scripts/mermaid-render.py write-path-map.json --out diagrams.md and paste the four diagrams into §9 of the main report. All four are required:
- A. System write flowchart
- B. Per-endpoint sequence diagrams (top 20)
- C. Data-domain write map (bipartite)
- D. DB trigger / function graph
- Render the standalone risk register. Use
templates/risk-register-template.md. Write to risk-register.md alongside the main report. Cross-link from §7 of the main report.
- Write artifacts. Using the
Write tool, create write-path-map.md, write-path-map.json, and risk-register.md at the project root (or under .claude/ if the project has one). Never overwrite an existing file without asking first.
- Emit completeness summary. Print the final tier and score to the chat response. Surface any phase that scored <80% as a known gap the user should rerun.
Phase completeness = required_report_sections_rendered / 11 × 100.
Completeness Summary
| Phase | Focus | Weight |
|---|
| Phase 1 — Discovery | Schema, triggers, cron, queues, ignore file | Phase-1 adjustments |
| Phase 2 — Entry points | Every write entry found | Coverage |
| Phase 3 — Middleware & auth | Auth layer recorded per entry | Depth |
| Phase 4 — Validators | Input validation recorded | Depth |
| Phase 5 — Persistence trace | Targets + transaction state | Coverage + depth |
| Phase 6 — Async / triggers | Queue consumers, cron, triggers | Coverage |
| Phase 7 — Risk analysis | Every risk subtype walked | Depth |
| Phase 8 — Reporting | All required sections rendered | Depth |
Tiers:
- 95–100 — FULLY MAPPED
- 80–94 — MOSTLY MAPPED (gaps listed)
- 60–79 — PARTIALLY MAPPED
- <60 — INSUFFICIENT — rerun with sub-agents or narrower scope
Important Principles
- The map is a hypothesis backed by trace evidence. Reports describe what the skill observed; they never refactor or delete code.
- This skill never modifies source files. It only emits three new artifacts (
write-path-map.md, write-path-map.json, risk-register.md) at the project root. If those files already exist, ask before overwriting.
- Every mapped path must carry: entry → middleware chain → validator → auth layer → handler → persistence target(s) → downstream effects. A path missing any of these is incomplete.
- Paths without a persistence target are not writes. Drop them or reclassify as reads. Listing GETs in the write map is a bug.
- Completeness measures trace thoroughness, not quality. A 100%-complete map of a broken system is still 100% complete. Quality is captured in the Risk Register.
- DB writes include triggers, functions, policies, and cron jobs. A write path is incomplete until DB-side effects are enumerated via Phase 6.
- Never trust dynamic dispatch silently. Flag
dynamic-dispatch-write wherever a write target is resolved at runtime and leave it as INFO for human review.
- Prefer live DB data when available. Static schema files can be out of sync with the live database. If Supabase MCP or
DATABASE_URL is available, enrich via Phase 1 step 7.
- Respect
.write-path-ignore. Treat entries as load-bearing and surface stale patterns as warnings.
- Sub-agents must be used aggressively. Phase 2 and Phase 5 MUST spawn parallel
Explore agents when the project has >30 candidates or any handler has 3+ service-layer hops. Cutting corners here directly reduces completeness.
- Document tool versions (ripgrep, Python, Node) in the report header so runs are reproducible.
Edge Cases
- Empty / prototype project. If fewer than 10 candidate entries exist, produce a minimal map without spawning sub-agents. Completeness tier "FULLY MAPPED" is achievable on small projects.
- Monorepo. Treat each workspace package as a separate mapping target and produce a per-package map plus a workspace-level rollup. Cross-package writes (a service in package A writing via an import from package B) become fan-out edges.
- Serverless / edge functions. Each function file is an entry point. Cold-start logic (auth clients, DB pool init) counts as middleware.
- GraphQL. Every mutation resolver is an entry point. Subscriptions are NOT writes unless they trigger DB publishes. GraphQL queries are never writes.
- Event-driven / CQRS. Commands are writes. Events emitted from commands are fan-out. Projections that write to read stores are secondary entry points.
- Multi-tenant. Missing
workspace_id / tenant_id filter on a scoped table is cross-tenant-leak CRITICAL. Verify every Supabase/ORM write against the table's column list.
- Background workers. Queue consumers are secondary entry points. Map both the producer and the consumer. Orphans (producer with no consumer, or vice versa) are flagged in Phase 6.
- DB triggers and functions. These are persistence-layer write paths with no application code entry point. Map them in Phase 6 from
extract-triggers.sh. Include them in the DB trigger graph (Diagram D).
- Dynamic SQL. Flag
sqli-risk if the skill detects string interpolation with user input. Flag dynamic-dispatch-write if routing is resolved at runtime. Never attempt to evaluate dynamic SQL — it's the human reviewer's responsibility.
- Generated clients (tRPC codegen, Prisma client, GraphQL codegen). Trace to the generator input (schema, router definition), not the generated output. Add generator directories to the suggested
.write-path-ignore.
- Pure read-only project. If zero write paths are detected, emit a "zero write paths detected" report with only Phase 1 output and exit cleanly. This is a valid outcome, not a failure.
- Unsupported language (Elixir, Haskell, OCaml, Crystal, Zig). Emit "stack not fully supported" in Phase 1 and dump the ripgrep seed list as the map without structural guarantees. Do not fabricate findings.
- Tool failures. If
ast-entrypoints.py or any other helper crashes, record it as a Phase 1 limitation and continue. The skill must never abort on a single-tool failure.