Threat modeler
15 production-grade Claude Code skills that turn it into a full-stack engineering agent — design, code, test, secure, ship. Also works with OpenAI Codex CLI. MIT.
npx -y skills add ak-ship/fullstack-agent-skills --skill threat-modelerAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Build a real threat model for a codebase, service, or feature — before the code review, before the pen test, before launch. Picks the right frame (STRIDE for systems, attack-tree for features, abuser-stories for product flows), works through assets / actors / entry points / trust boundaries via interview + code-read, and produces a structured THREAT_MODEL.md that downstream skills can use to scope a targeted scan. Use when the user says "threat model this", "what's the attack surface", "do a STRIDE analysis", "what could go wrong here", "model the threats", or before any launch touching auth, payments, PII, or multi-tenancy.
SKILL.md
16.0 KB, as published. Nobody here has run it
threat-modeler — figure out what to defend before you defend it
When to use this skill
Trigger when the user wants a planned security view, not a generic scan. Strong signals:
- "threat model this", "do a STRIDE analysis", "what's the attack surface"
- "what could an attacker do here"
- Before any new feature touching auth, payments, file uploads, PII, multi-tenancy, or webhooks
- Before a SOC 2 / ISO 27001 / pen-test engagement
- A new service is being designed and there's no security context yet
Do not trigger for: a static code review (use security-sentinel), incident response (you need a responder), or generic "make this secure" requests where no specific surface has been picked. Pin a scope first.
The output contract
A THREAT_MODEL.md file with two layers:
- A machine-readable section (front matter or JSON block) — downstream skills (
security-sentinel,code-auditor) can parse it to scope what they look for. Lists assets, actors, entry points, trust boundaries, and threats with severity scores. - A human-readable section — the actual narrative. A new engineer should be able to read it in 15 minutes and understand what's worth attacking, what's worth defending, and where the team has deliberately accepted a risk.
Plus three concrete deliverables alongside the doc:
- An assumption log — every claim the model rests on, so the next reviewer can verify or invalidate
- An open-questions list — the things the interview couldn't resolve; goes back to the user or to engineering
- A top-5 risk list — the five things to fix this sprint, ranked by likelihood × impact
If the model has 80 threats and no ranking, it's a list, not a model. Rank or don't ship.
Workflow
1 — Pick the frame
Different surfaces fit different frames. Pick one before starting:
- STRIDE (Spoofing, Tampering, Repudiation, Information disclosure, DoS, Elevation of privilege) — for systems and services. The default for "model this API".
- Attack-tree — for one specific high-value flow (payment, signing, identity proofing). The root is the attacker goal; the branches are how they'd get there.
- Abuser stories — for products with end users (multi-tenant SaaS, marketplaces, social). Mirror image of user stories: "as a malicious tenant I want to see other tenants' data so that…"
- LINDDUN — for privacy-heavy systems (Linkability, Identifiability, Non-repudiation, Detectability, Disclosure, Unawareness, Non-compliance). Pull this in in addition to STRIDE when GDPR/HIPAA/biometric data is on the table.
State the choice in one line in the doc. If a system has two distinct surfaces (e.g., a public API + an admin app), do two passes with two frames.
2 — Bootstrap from code (if there is code)
Before asking the user anything, read the code and infer what you can. Goal: walk into the interview with informed questions, not a blank slate.
This pack is web-app focused — the grep recipes below assume Node.js + React (Express / Fastify / Next.js / NestJS on the server; React / Vue / Svelte on the client). For other stacks adapt the patterns; the categories are universal.
For each entry point, identify:
HTTP routes — what's public, what's authenticated, what's admin-only
# Express / Fastify route surface
rg -t ts -t js '\.(get|post|put|patch|delete)\s*\(' src/ | head -40
# Next.js app router
fd 'route\.(ts|js)' app/ | head -20
# Per-route auth middleware (or lack thereof)
rg -B2 'requireAuth|withAuth|isAuthenticated|@UseGuards' src/
Background jobs / cron — what runs on its own authority
rg -t ts -t js 'cron|bullmq|agenda|node-cron|setInterval|setTimeout.*hours?\)'
Webhook handlers — what external callers can trigger
rg -t ts -t js 'webhook|/hooks/|stripe.webhook|sendgrid|svix' src/
Client-side trust — what the React layer assumes about server response
# dangerouslySetInnerHTML is the canonical XSS surface in React
rg 'dangerouslySetInnerHTML' src/
# direct DOM writes that bypass React
rg 'innerHTML\s*=|document\.write' src/
# routes that take user input directly into the URL
rg 'window\.location|router\.push\([^,)]*(query|params)'
File / blob storage — what's user-controlled, what's served back to other users
rg -t ts -t js 'multer|formidable|busboy|S3.*Upload|presigned'
Outbound calls — where user input flows out to other services (SSRF surface)
rg -t ts -t js 'fetch\(|axios\.|undici|got\(' src/ | rg 'req\.|body\.|query\.'
Database access — what queries take user input
# raw SQL with template strings = parameterization audit
rg -t ts -t js 'raw\(|query\(`|sql`' src/
# Mongo: user-controlled keys can become $-operators
rg -t ts -t js '\.find\(.*req\.body|\.findOne\(.*req\.body' src/
Secrets — what's in env vs hardcoded
rg -t ts -t js 'process\.env\.' src/ | head -20
# anything that looks like a baked-in key
rg -t ts -t js -P '(sk_live|sk_test|AIza|AKIA|ghp_)[A-Za-z0-9_-]+'
Build a one-page "what I see" summary. This is not the threat model yet. It's the raw map.
3 — Interview the user to fill gaps
The code can't tell you:
- Who's a threat? External attackers, malicious users, malicious tenants in a shared platform, compromised employees, supply-chain attackers, hostile nation-states, customers' angry exes (real for consumer apps), automated scrapers?
- What's actually sensitive? Code doesn't know "this column holds adoption records sealed by court order" vs "this column holds usernames".
- What's the blast radius if X is breached? Regulatory? Reputational? Existential?
- What's been deliberately accepted? "Yes we know the admin app trusts the network — we're inside the VPN and that's by design." Threats that have been considered and accepted are not findings.
Run the interview short and pointed (see interview-guide.md for the question bank). Cap it at 30 minutes — if there's a 90-minute discussion to be had, surface that as "needs follow-up" and don't try to do it inside the threat model.
4 — Map the four pillars
Now write the structured part of the doc:
- Assets: what's worth protecting. Be specific. "User PII" is lazy; "billing addresses, IP at signup time, support-chat transcripts" is real.
- Actors: who would attack and why. Internal vs external. Authenticated vs anonymous. Skilled (nation-state, organized fraud) vs opportunistic (a script kiddie hitting your signup endpoint).
- Entry points: every place untrusted data enters the system. Every. Place. HTTP, GraphQL, webhooks, file upload, OAuth callback, email parsing, push notification tokens, support-form attachments.
- Trust boundaries: where authority shifts. The browser ⇄ backend boundary. Backend ⇄ third-party API. Application ⇄ database. Tenant A ⇄ tenant B. Free user ⇄ paid user ⇄ admin.
Each pillar gets a short numbered list. No paragraphs. The structure exists to be queried.
5 — Walk the frame
For STRIDE: for each asset, for each relevant entry point, ask the six questions:
| Letter | Threat | Example for "user profile API" |
|---|---|---|
| S | Spoofing | Can someone impersonate another user? (Stolen token, session fixation, password reset weakness) |
| T | Tampering | Can data be modified by someone who shouldn't? (IDOR on PUT, mass-assignment, missing signature on webhook) |
| R | Repudiation | Can a real action be denied? (No audit log on sensitive changes; logs trivially editable) |
| I | Information disclosure | Can data leak to someone who shouldn't see it? (IDOR on GET, verbose errors, side-channels) |
| D | DoS | Can someone make this unavailable? (Unbounded query, no rate limit, expensive endpoints without auth) |
| E | Elevation of privilege | Can a low-priv user become high-priv? (Role check missing on admin route; JWT alg confusion) |
Skip questions that genuinely don't apply ("repudiation isn't relevant to this read-only public endpoint") and say so — silence reads as oversight, but an explicit "N/A: this endpoint is unauthenticated and stateless" reads as rigor.
See stride-reference.md for the full prompt bank per letter, including the modern web concerns STRIDE alone doesn't surface (SSRF, deserialization, subdomain takeover, OAuth scope creep, race conditions on state machines).
6 — Score and rank
For each threat:
- Likelihood: 1 (theoretical) → 5 (someone will try this in the first month)
- Impact: 1 (annoyance) → 5 (existential)
- Severity = likelihood × impact (1–25)
- Mitigation status: existing / partial / none
Sort by severity. Top 5 go into the "fix this sprint" list. The rest sit in the model as known risks with planned mitigations or accepted-risk notes.
Be honest about likelihood. A SQL injection in code that doesn't touch user input is likelihood 1 even if impact is 5. A missing rate limit on /login is likelihood 5 because someone is going to try it tomorrow.
7 — Capture assumptions and open questions
The model is only valid under specific assumptions. Write them down explicitly:
- "Assumes the admin app is only reachable from the corp VPN."
- "Assumes Stripe is the only PCI-handling component."
- "Assumes session tokens in cookies are sufficient for CSRF protection because of SameSite=Lax."
If any assumption flips later (admin app gets a public endpoint; you start handling PANs; you switch to a SPA on a different domain), the model needs a re-run. Note it.
Open questions go in their own section, each with an owner. "Who actually has access to the prod DB read-replica?" is a question for ops, not a guess for the model.
8 — Hand off
Done. The doc is the artifact. Now:
- Use it as input to
security-sentinel— "scan with priority on the top-5 threats from THREAT_MODEL.md" - Use it as input to
api-architectfor new endpoints — every new entry point inherits the boundary controls the model demands - Re-run quarterly or when any assumption changes (whichever is sooner)
Patterns and anti-patterns
✅ Do:
- Threat-model the change, not the whole company. A model for "the new invitations feature" is more useful than a model for "all of our SaaS".
- Name assumptions explicitly. The doc's job is to be invalidated later; named assumptions make that easy.
- Include the things you aren't worried about and why. The absence of a concern reads as oversight; the explicit decision reads as rigor.
- Walk the model with the team after writing it. A model the team disagrees with is more useful than one they merely nod at.
❌ Don't:
- Don't write a generic threat model from a template. If yours could equally describe Slack and Stripe, it's describing nothing.
- Don't list 80 threats without ranking. Pasta isn't a model.
- Don't conflate threat (what attacker would do) with vulnerability (how the code lets them). Threat → goal; vulnerability → mechanism. The model is the goals; the scan finds the mechanisms.
- Don't model "an APT with unlimited budget" if you're a 5-person company shipping a SaaS. Pick threats proportional to who actually targets you.
- Don't make the model a one-time artifact. A model that hasn't been touched in a year is decoration.
Example invocation
User: "We're shipping an invitations feature next week. Threat-model it."
- Frame: STRIDE for the invitation endpoints; abuser stories for the user-facing accept page.
- Bootstrap: read
src/api/invitations/— 5 endpoints, JWT auth on 3, public + token-based on 2. Webhook handler for email-bounce notifications. Send-email goes through a third-party (SendGrid). - Interview (15 min with the user):
- Sensitive assets: pending invitation tokens (one-time signup grant), email addresses (PII), inviter identity.
- Actors of concern: random external attackers, malicious users trying to invite themselves to other orgs, scrapers harvesting valid emails.
- Trust boundaries: browser ⇄ public-accept endpoint; backend ⇄ SendGrid webhook; tenant A ⇄ tenant B.
- Accepted risks: "We don't try to prevent users from inviting fake emails — we expect bounce rates."
- Pillars:
- Assets: invitation token (high), org membership grant (high), email address (medium)
- Actors: ext-anon, ext-authenticated-other-org, malicious-tenant, automated-scraper, sendgrid-compromise
- Entry points: POST /invitations (auth), GET /invitations/by-token/:t (public+rl), POST /accept (public+token), POST /webhooks/sendgrid (public+sig)
- Trust boundaries: 3 listed above
- STRIDE walk — 14 threats identified, e.g.:
- S Token-guessing on /by-token → likelihood 4, impact 4 (sev 16): need 128-bit token + rate limit
- I Enumeration via "email already invited" error → likelihood 5, impact 2 (sev 10): return same response either way
- R No audit log on accept → likelihood 3, impact 3 (sev 9): log to existing audit table
- E Mass-assignment on invitation create lets a regular user set
role: admin→ likelihood 4, impact 5 (sev 20): strict allowlist in the handler - D No rate limit on /by-token enables 100k req/min crawl → likelihood 4, impact 3 (sev 12): rate-limit by IP and by token-prefix
- T SendGrid webhook signature check is currently absent → likelihood 3, impact 4 (sev 12): verify signature with
crypto.timingSafeEqual
- Top-5 ranked for the sprint: mass-assignment, token-guessing, sendgrid signature, rate limit on /by-token, enumeration.
- Assumptions: tokens are 128-bit random from
crypto.randomBytes; SendGrid is the only path; accept endpoint always requires an authenticated session. - Open questions: do we want a "this invitation has been forwarded" detection? (privacy team to decide).
- Output:
docs/security/THREAT_MODEL-invitations.mdwith all of the above, and a structured front-matter block forsecurity-sentinelto consume.
See also
security-sentinel— runs the targeted scan informed by this modelapi-architect— sets the auth/boundary controls the model demands for new endpointscode-auditor— finds the specific bugs corresponding to the threats this model namedresilience-engineer— many tampering/repudiation threats are closed by typed errors + structured logging- Reference files:
stride-reference.md,interview-guide.md
Compatibility
Works in both Claude Code (native SKILL.md auto-routing) and OpenAI Codex CLI (via the for-codex/AGENTS.md routing table). No code execution required — this is an interactive skill that reads files and asks questions; safe to run in any repo.
Attribution
The structural pattern (split planning from finding; bootstrap-from-code then interview; rank by likelihood × impact) is inspired by Anthropic's open-source defending-code-reference-harness, specifically how it separates a planning skill (/threat-model) from a finding skill (/vuln-scan). That repo is Apache 2.0, focused on C/C++ memory bugs.
This skill targets the React + Node.js / TypeScript stack the rest of fullstack-agent-skills is built for. Every workflow step, prompt, grep recipe, STRIDE prompt-bank, interview question, and example is written from first principles for web apps — JWT and OAuth pitfalls, IDOR and mass-assignment, SSRF and CORS, prototype pollution, Next.js getServerSideProps leaks, multi-tenant row-level guards. MIT licensed.