Your project needs a threat model
🇧🇷 Leia em Português
Today I added a practice to our manifesto in the Codeminer42 wiki: every project must keep a threat model in its repository. During that conversation, I found that many people weren’t familiar with the concept.
Asking for the document isn’t enough. People need to know what it records and how to start. Public examples and the skill we’ve made available can help with both.
1. Secure against what?
If you tell me a system is secure, I still need more information. Secure against someone on the internet? Another authenticated customer? A compromised support account? What could that person read, change, or interrupt?
A threat model describes a system from a security perspective. It records what we want to protect, who could cause harm, what assumptions we make, and how we address threats. It also states what the system does not protect against.
Think of an apartment building with a doorman. Someone gets past the entrance. Does that give them access to every apartment? A delivery driver, a resident, and someone with a stolen key can reach different places. Saying “the building has a doorman” doesn’t tell you which doors each person can open.
In software, “it has a login” leaves the same question unanswered.
Adam Shostack organizes the process around four questions, also adopted by the Threat Modeling Manifesto:
- What are we working on?
- What can go wrong?
- What are we going to do about it?
- Did we do a good enough job?
The last question takes you back to the others. Did you change an integration, start storing personal data, or open an API to a different audience? Check whether the model still describes the system.
2. What goes in the document
Imagine an application where several companies store documents, each with its own users. This is a fictional example.
The documents are assets, things we want to protect. User sessions and application availability belong on that list too. For each asset, write down what happens if protection fails: a document leaks, someone uses another person’s session, or everyone loses the ability to work.
Actors include anonymous visitors, authenticated users, administrators, and external services. A legitimate account can be used to cause harm. “User” and “hacker” aren’t enough to describe everyone involved.
Trust boundaries are the points where permissions or assumptions about data change. Sending an ID from the browser does not authorize the server to return that document. The backend needs to check whether the user can access it.
Browser -> API -> Database
|
+-> External processing service
This diagram is a start, but the boxes aren’t enough. Who writes to the database? What gets sent to the external service? Can its response determine who gets access? Two processes on the same network can sit on opposite sides of a trust boundary.
In this example, the threat is a user from one company reading another company’s documents. A missing membership check would be a vulnerability: the weakness that allows that access. Risk combines the likelihood of this happening with the impact it would have, taking existing controls into account.
That’s why the document needs to go beyond a bug list. Fixing the query closes that vulnerability, but the threat remains relevant. A new route could allow the same unauthorized access.
The model also records controls that already work. In our example, the API checks the user’s membership in the company before fetching the document. Write down where that check happens and where it doesn’t. Protecting the HTTP route doesn’t guarantee that a worker or a WebSocket channel applies the same rule.
For a systematic way to look for threats, OWASP describes STRIDE: Spoofing, Tampering, Repudiation, Information disclosure, Denial of service, and Elevation of privilege. These categories guide questions about each boundary. They aren’t six fields to fill with whatever comes to mind.
NOTE: you can build a threat model during design, before any code exists. For a running system, reading the implementation helps check assumptions against what was actually built. It does not replace tests, audits, or penetration testing.
3. Two public examples
ai-memory: storing text doesn’t make it trustworthy
ai-memory, by Fabio Akita, stores long-term memory for coding agents. Its SECURITY.md includes a threat model with the premise, what’s in scope, and what’s outside it.
One of the threats it describes is prompt injection through stored content. Malicious text enters memory and later returns in a search result or a handoff between agents. If the agent follows that text as an instruction, memory has carried the attack to it.
The document explains that the system marks retrieved content as untrusted historical data. Sanitization and structured formats help, but they do not prove that the model will ignore every manipulation attempt. The description of the defense states its limits too.
The data directory relies on operating-system permissions; the application provides no encryption at rest of its own. The service was not designed for direct exposure to the hostile internet either.
In the version I consulted, projects are open by default, and the operator can restrict access per project for users registered in the database. Within a project, however, there are no per-page permissions. Identities supplied by a trusted proxy follow a different authorization boundary, described in the document.
If you need two people to have access only to specific pages within the same project, that control won’t meet your needs. Supporting multiple users doesn’t establish that kind of isolation.
Syncthing: content and metadata are different things
Syncthing synchronizes files between devices. Its Security Principles page doesn’t use the title “Threat Model,” but it records threats, controls, and assumptions that serve that purpose.
Traffic between devices uses TLS, and the connection checks the device’s identity against a list of accepted devices. A relay server cannot inspect the transmitted content. It does, however, know the identifiers of the devices it connects. With global discovery enabled, the discovery server operator can map identifiers to IP addresses and infer connections.
Protecting content doesn’t mean hiding that you use the system or which peers your devices connect to.
The documentation also explains that someone who obtains a device’s TLS keys and configuration can impersonate it and access synchronized files on its peers. It therefore recommends revoking access for lost devices and protecting the configuration directory, not just the synchronized files.
Asking whether the relay operator can read your files is different from asking what they know about the connection, or what happens if someone steals your laptop’s keys. The product is the same, but the answers and controls differ.
4. A skill for writing the first version
We’ve published threat-model in Codeminer42’s public skills repository. You can install it with:
npx skills add Codeminer42/skills --skill threat-model
Then, from a repository you are authorized to analyze, ask your agent:
Use the threat-model skill to create this project's threat model.
Confirm the premise and scope with me before writing it.
The skill first looks for an existing model. If the project or client already maintains one, it respects that format and updates it only when asked. It also checks the repository’s visibility before writing open threats into it.
Next, it agrees on the premise with you: which applications are in scope, who can reach them, which external systems write to the data, and what would hurt most to lose. People who know the business need to answer. Code doesn’t decide which risks the team accepts.
The agent then reads the routes and their authentication and authorization checks. It examines databases, integrations, files, configuration, and logs, along with the build and deployment process. When subagents are available, it divides that work by area, then checks the evidence and merges findings that belong to the same threat.
The result is a draft in docs/threat-model.md, with assets, actors, boundaries, threats, and controls. Each threat must cite the files and functions that support the analysis. The skill also links to the document from the agent instructions, such as AGENTS.md, so the agent consults it when making later changes.
When the skill ran on real projects, the resulting documents separated code evidence from unanswered questions about the environment. They also recorded the limits of existing controls. The team gets specific things to check rather than a generic security verdict.
Suppose the agent reads a route with no authentication but cannot see the network access rules. The document needs to separate what the code demonstrates from what depends on configuration. “I didn’t find this rule in the repository” does not mean “I confirmed that it doesn’t exist in production.”
The skill distinguishes these cases. If the code already demonstrates the problem, the threat can be marked Open, with a Verify line for anything still to check. If the harm depends on configuration or behavior that the code doesn’t show, it is marked To verify.
The skill does not fix code, test deployed environments, accept risks, or make commits. You get a draft to review, not a completed audit. Someone needs to open the evidence for each threat, check the assumptions, and discuss severity with the team.
5. Use the document in everyday work
In the format we’ve adopted, assets get IDs such as A1, actors X1, boundaries B1, and threats T1. These IDs are never renumbered or reused.
That lets you write a PR description like this:
This change touches B1 and mitigates T1.
The query now checks company membership before returning the document.
The test covers an access attempt by a user from another company.
This PR example is fictional too. With those IDs, a reviewer can open T1, understand the threat, and check whether the change does what it claims.
A mitigated threat stays in the model with the status Mitigated and a reference to the fix. If the team decides to live with the risk, the status becomes Accepted, with an ADR, an Architecture Decision Record, explaining why. The agent can suggest a mitigation, but the people responsible for the system must decide whether to accept the risk.
We also record where the fix belongs. Sometimes everything can be fixed in this repository. In other cases, part of the change depends on an integration, access configuration, or another team. The model needs to say what depends on that external fix and what the application can do to limit the damage in the meantime.
Whoever adds an integration, route, or shared table updates the boundaries in the same PR. During code review, we consult the existing controls: an apparently harmless simplification might remove the check that prevents a known threat.
NOTE: keeping the model alongside the code does not mean publishing open vulnerabilities. In public repositories, premises, controls, and limits can be public while sensitive findings follow a private security process. The skill asks before writing those threats. Do not put credentials, personal data, or client details in the document.
Start with what you know
For a new project, start with the premise, a small diagram, and the risks in the first flows. If the system already exists, describe how it works today and record the unanswered questions. If the client already maintains a model, use it instead of creating a competing one.
Even an incomplete document helps. You record who writes to the data, who can read it, and which checks control that access. You can then discuss a change more precisely than “I think it’s secure.”
There may still be threats nobody has considered. But the team has a record of what it has examined, what it has verified, and what it still needs to resolve.
Thanks for reading!