Security reference — the security lens on every phase gate
Security is a first-class, gated concern of the spec workflow, not a bolt-on step: each existing phase gate (requirements → design → review) also checks a security question, and the answer is recorded in the phase's artifact. Always on — the security block left .the-loop/harness-config.yaml in issue-352 because nothing here is optional; born from the prompt-injection finding on the trigger paths (decision-023 → issue-47).
The principle behind every check: untrusted input never drives privileged behaviour, and ambiguity fails closed.
Requirements phase — threat-model-lite (always required)
requirements.md (and bugfix.md) carries a Security considerations section — a lightweight threat model captured alongside the acceptance criteria, while scope is still cheap to change:
- Actors & trust: who interacts with the feature, and which of them are untrusted (anonymous users, third-party commenters, webhook payloads, fetched content…).
- Trust boundaries & data: where untrusted data crosses into trusted behaviour, and what sensitive data (secrets, tokens, PII) is stored/moved.
- Abuse cases: how a hostile actor would misuse the feature, as EARS criteria (
WHEN <hostile event> THEN the system SHALL <safe response>) — these become negative tests. - Fail-closed expectations: what must be rejected when identity/authorization/ configuration is missing or ambiguous.
"No new attack surface" is a valid answer — but it is written down and justified (e.g. "pure refactor, no new inputs, no new privileges"), never implied by omission. A work item with an empty Security considerations section does not pass the requirements gate.
The threat model is per work item: each section records its own actors, boundaries and abuse cases; there is no project-level threat-model doc to point at instead.
Design phase — enforce the boundaries (always required)
design.md carries a Security design section stating how each trust boundary from the requirements is enforced — mechanisms, not intentions:
- AuthN/AuthZ: who is identified how, and where authorization is checked (the authorized-actor guard of decision-023 is the house example).
- Input validation & injection surfaces: every untrusted ingress and its validation/encoding; SQL/command/path/prompt injection surfaces named explicitly.
- Secrets handling: where secrets come from (env/secret store, never the repo or logs) and where they must never appear.
- Least privilege: the minimum permissions/scopes/capabilities each component runs with.
- Fail-closed behaviour: the concrete response when a check cannot be made.
- Abuse-case coverage: each abuse case from the requirements mapped to the mechanism that defeats it and the test that proves it (feeds the testing strategy).
A design that leaves a requirements-phase trust boundary unenforced does not pass the design gate.
Implementation — abuse cases are tests
Abuse cases follow the same TDD invariant as everything else: security-relevant tasks in tasks.md name the negative test that proves the boundary holds (unauthorized actor rejected, malformed input refused, missing config fails closed), red→green like any other task.
Review phase — the security review gate (always required)
Before the ready-to-ship gate can hold, a security review runs as its own recorded round — complementing (not replacing) the self/critic rounds of reviewing.md.
The
security-reviewnode is selectable (issue-179, decision-068): an authorized human may declare it away atphase-selection, before any work starts, and the omission is recorded against their name. The graph no longer refuses it. Nothing else changed — a session never declares it away itself, and when the node is walked it gatesevidence/security-review.mdexactly as below. A work item at risk tier 4 or above still requires a named human sign-off; that policy is now upheld by the person selecting the phases, not by the graph.
- Mechanism: the harness's built-in security-review skill (e.g. Claude Code's
/security-review) when it has one; otherwise the checklist below. Nothing selects between them — a harness that has the skill uses it. - Findings follow the standard protocol: reply-first-then-fix, one finding per commit (
reviewing.md). A security finding is never silently dismissed — won't-fix requires a recorded justification, and an unresolved security finding blocks completion regardless of risk tier. - Record the round in
evidence/security-review.md— the mechanism used, the outcome, each finding with its disposition, and the human sign-off where the risk tier requires one. The node blocks until that section is written.
The checklist (fallback mechanism)
Verify against the diff, not from memory — each item pass/fail with evidence:
- Every trust boundary in
design.md§Security design is enforced where the design says it is. - All untrusted inputs are validated/constrained at their ingress; named injection surfaces (SQL/command/path/prompt) are covered.
- Untrusted content cannot steer privileged behaviour (prompt-injection posture: authorized-actor guards, untrusted-data framing).
- No secrets in code, config, logs or fixtures; secrets come from env/secret store.
- AuthZ checks fail closed — missing identity/allowlist/config denies, never allows.
- Components run with least privilege (scopes, file access, network).
- Every abuse case from the requirements has a passing negative test.
- New dependencies are justified in
design.mdand come from trusted sources.
Human sign-off — tier 4 and above
The autonomous checklist is enough for low-risk work; high-risk work waits for a human (same shape as the risk tiers in workflow.md):
- Compute the work item's effective risk tier as usual:
riskTierfront-matter, else inferred from the change (default 3 when unclear), raised when the change touches a fixed sensitive path —**/*schema*,.the-loop/**,.github/workflows/**,**/auth/**,**/*secret*,**/*credential*. - Tier 4 and above: a named human must approve the security review (paper trail on the PR/ticket) before the work item completes — a distinct sign-off from the PR approval the tier already requires; autonomous completion is off the table.
- Tier 3 and below: the autonomous security review suffices; escalate anyway when a finding needs a security-relevant decision (accepting a residual risk, weakening a guard, adding an allowlist entry) — decisions belong to humans, detection belongs to the loop.
Recording
Durable security decisions go to docs/decisions/ like any other decision (decision-023 is the template case). Learnings from findings feed the learnings lifecycle so recurring classes of issue become checklist knowledge.