diff --git a/.github/SECURITY.md b/.github/SECURITY.md
index ae419c1..e58db90 100644
--- a/.github/SECURITY.md
+++ b/.github/SECURITY.md
@@ -1,9 +1,56 @@
-# Security Policy
+# Security policy
-## Supported Versions
+python-discovery is a Python library that finds Python interpreters on a machine and reports their metadata. This policy
+says how to report a security problem and how we decide whether a report is one.
-Only the latest released version is supported with security updates. Older versions will not receive patches.
+## Reporting a vulnerability
-## Reporting a Vulnerability
+**Do not open a public issue for a security problem.** Use
+[GitHub's private vulnerability reporting](https://github.com/tox-dev/python-discovery/security/advisories/new). If
+neither works for you, email [gaborjbernat@gmail.com](mailto:gaborjbernat@gmail.com).
-Please report security vulnerabilities via [GitHub's private vulnerability reporting](../../security/advisories/new).
+Include what the attacker controls and what they gain, the steps or a proof of concept, and the versions and platform
+you tested. Write the report yourself; unreviewed tool or model output takes longer to triage.
+
+## What to expect
+
+Volunteers maintain python-discovery, with no on-call rotation. We aim to acknowledge a report within **5 working days**
+and assess it within **15 working days**. Fixes are developed in a private fork attached to the draft advisory, ship in
+a new release, and the advisory, with a CVE requested through GitHub, goes out with that release. The default embargo is
+**90 days** from the report; we agree any change with the reporter. We credit reporters in the advisory under the name
+they choose, or not at all. There is no bug bounty.
+
+## Supported versions
+
+Only the latest release receives fixes. Confirm the problem reproduces on it before reporting.
+
+## Vulnerability or bug
+
+A report is a vulnerability when an attacker who controls only an input that the [threat model](THREAT_MODEL.md) lists
+as untrusted breaks a guarantee python-discovery makes. A report that first needs control of something the threat model
+trusts, such as an executable on `PATH` or in a version manager's directory, is a bug: we fix it in the open and publish
+no advisory. Classify by who supplies the dangerous input, not by how bad the outcome sounds.
+
+**In scope**
+
+- Executing a binary from a location the documented search order does not include, such as the current directory through
+ an empty `PATH` entry.
+- A cache entry from one interpreter being reported for another, or surviving a change of the binary.
+- Interrogation output from one candidate being parsed as another candidate's metadata.
+- A spec's version, implementation, architecture, free-threading or debug constraint passing for a shim or launcher
+ while the interpreter it resolves to fails it.
+- A PEP 514 record or a spec making discovery run a program other than the candidate with its registered arguments.
+- Another local user writing a shared cache directory so a victim accepts an interpreter, or metadata, that discovery
+ would otherwise reject.
+- A small candidate response exhausting memory or processes.
+
+**Out of scope**
+
+- A malicious interpreter in a searched location: python-discovery runs candidates to learn about them, so whoever can
+ place one there already runs code as the user.
+- Values in the process environment (`PATH`, `PYENV_ROOT`, `UV_PYTHON_INSTALL_DIR`, `PY_DISCOVERY_TIMEOUT`) and the
+ Windows registry entries of PEP 514.
+- A `.python-version` file choosing among interpreters that discovery would find anyway.
+- Tampering with a cache directory only the caller can write.
+
+If you cannot tell which side a finding falls on, report it privately and we will decide.
diff --git a/.github/THREAT_MODEL.md b/.github/THREAT_MODEL.md
new file mode 100644
index 0000000..9760750
--- /dev/null
+++ b/.github/THREAT_MODEL.md
@@ -0,0 +1,106 @@
+# Threat model
+
+This model says what python-discovery trusts, where untrusted input enters, and which guarantees hold at each boundary,
+so a report can be classified as a vulnerability or a bug. [SECURITY.md](SECURITY.md) covers reporting; if the two
+disagree, SECURITY.md wins and we fix this file.
+
+## How python-discovery works
+
+A caller passes a requirement such as `python3.12` or `>=3.11,<3.13`. python-discovery walks `try_first_with` paths, the
+running interpreter, the PEP 514 registry on Windows, `PATH` (resolving pyenv, mise and asdf shims to the binary they
+select), and uv's install directories. It runs each candidate with a bundled interrogation script and reads a JSON
+answer framed by two random cookies, then caches that answer in a directory the caller supplies, valid while the
+binary's path and modification time, the script's hash and any PEP 514 arguments stay the same.
+
+```mermaid
+flowchart LR
+ caller["Caller
spec, env, cache dir (trusted)"] --> pd["python-discovery"]
+ pd -->|search| locs[("PATH, version managers,
PEP 514 registry (trusted)")]
+ pd -->|"run candidate + script"| py["Candidate interpreter"]
+ py -->|"JSON between cookies"| pd
+ pd <-->|"checked against path, mtime,
script hash, PEP 514 args"| cache[("Cache dir")]
+ classDef trusted fill:#cfe4ff,stroke:#1f6feb,color:#0b1f3a;
+ classDef boundary fill:#ffe3a3,stroke:#d29200,color:#3a2c00;
+ class caller,pd,locs trusted;
+ class py,cache boundary;
+```
+
+## Compared with similar tools
+
+We drew the boundary where comparable projects in other ecosystems draw theirs. The shared rule, stated by the
+[Node.js threat model](https://github.com/nodejs/node/blob/main/SECURITY.md#the-nodejs-threat-model), is that a report
+which first needs a trusted element compromised is out of scope; the first triage question, as
+[DOMPurify](https://github.com/cure53/DOMPurify/blob/main/SECURITY.md#what-we-consider-a-vulnerability) puts it, is what
+the attacker controls.
+
+| Project | Language | Where it draws the line |
+| ---------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------- |
+| [Go `os/exec` path lookup](https://go.dev/blog/path-security) | Go | Refuses to run a binary found through the current directory; python-discovery skips empty `PATH` entries for the same reason. |
+| [nvm](https://github.com/nvm-sh/nvm/blob/HEAD/.github/THREAT_MODEL.md) | Shell | Trusts what it installs and executes as the user; treats mirror metadata, not payloads, as the untrusted boundary. |
+| [uv](https://github.com/astral-sh/uv/security/policy) | Rust | Its policy says invoking the Python interpreters it finds, to read their metadata, is not a vulnerability. |
+
+## Trusted
+
+An attacker who controls any of these already has what python-discovery could give them:
+
+- The caller and the spec, environment mapping, cache directory and `try_first_with` paths it passes.
+- Every directory searched: `PATH` entries, version-manager roots and install directories, and the PEP 514 registry,
+ including `ExecutableArguments`.
+- The interpreters found there; running them is how discovery works.
+- A cache directory that only the caller can write (for example virtualenv's app-data).
+
+## Untrusted inputs
+
+| Input | Comes from | What python-discovery guarantees |
+| ----------------------------- | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
+| The current working directory | Wherever the caller happens to run | Empty `PATH` components, which shells read as the current directory, are skipped. |
+| Interrogation stdout | Startup noise or a `sitecustomize` that prints | Only the JSON between two per-run random cookies is parsed; anything else is a failed candidate. |
+| Cache entries | Earlier runs, possibly of another interpreter build | Reused only when the binary's path and modification time, the script hash and the PEP 514 arguments all match. |
+| A shared cache directory | Another local user who can write it | Must not make a victim accept an interpreter or metadata that discovery would reject; report any way it does. |
+
+## Threats and controls
+
+| STRIDE | Threat | Control |
+| ---------------------- | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
+| Spoofing | Output printed by a candidate's startup code impersonates its metadata. | Random start and end cookies frame the answer. |
+| Tampering | A stale cache entry describes a binary that has since been replaced. | The entry records the binary's modification time, the script's hash and the PEP 514 arguments. |
+| Denial of service | A candidate hangs during interrogation. | The interrogation has a timeout, configurable with `PY_DISCOVERY_TIMEOUT`. |
+| Elevation of privilege | A binary in the current directory is picked up through `PATH`. | Empty `PATH` entries are skipped. |
+
+## Release chain
+
+Releases run on the shared [tox-dev/github-workflows](https://github.com/tox-dev/github-workflows) pipeline. A release
+App, not a person, pushes the release commit and tag; the build installs its tools from a hash-checked `pylock.toml`
+resolved with a seven-day upload cooldown by default; PyPI receives the files through
+[trusted publishing](https://docs.pypi.org/trusted-publishers/) with attestations, and the GitHub Release is immutable.
+Every workflow pins third-party actions to a commit SHA.
+
+## Vulnerability or bug
+
+| Report | Verdict | Why |
+| --------------------------------------------------------------------------- | ------------------- | ---------------------------------------------------------------------------------------------------- |
+| A `python3` in the working directory runs because `PATH` ends with `:` | Vulnerability | The empty entry must not reach the current directory. |
+| A `pyenv` shim that is malware runs during discovery | Not a vulnerability | Searched locations are trusted. |
+| A cached answer for `/usr/bin/python3` is returned after the OS upgraded it | Bug | Cache correctness, crossing no boundary; a vulnerability only if it can point at a different binary. |
+| `PY_DISCOVERY_TIMEOUT=inf` lets a hung interpreter block forever | Not a vulnerability | The environment is trusted and the value is documented. |
+
+## Severity
+
+- **High:** argument handling runs a separate command the attacker chose, or another local user poisons a shared cache
+ so a victim runs an interpreter outside the requested constraints, without control of the victim's `PATH`, registry or
+ interpreters.
+- **Medium:** cache poisoning changes security-relevant metadata without execution, or a small candidate response causes
+ sustained memory or process exhaustion.
+- **Low:** the wrong interpreter selected without crossing a privilege boundary, a crash on malformed output, or stale
+ metadata from a cache only the caller writes.
+
+## Accepted risks
+
+- Discovery executes every candidate it considers; it cannot learn an interpreter's version without running it.
+- PEP 514 `ExecutableArguments` are raw command-line text from the registry, passed to the candidate as written, without
+ a shell.
+
+## Keeping this current
+
+Revisit this model when python-discovery gains a new input source, network access, a new way to run code, or a new place
+it writes to, and whenever an advisory is published.