Skip to content

The platform is live.

Take a look
The Lab

The method · 40 checks · 8 principles

What I actually check

The Trust Audit says what a system should do. This is the method underneath it: the questions I put to a real codebase, what clears each one, and the shape that fails.

A rubric is easy to agree with and impossible to act on. So here is the whole method, in the open. Eight principles, each one broken into the concrete questions I ask of your repository, what a passing answer looks like, and the anti-pattern I am looking for. Nothing here is generated. It is the list I work from.

Each check names where to look. Most are answerable by reading a handful of files, which is the point: a check you cannot verify is an opinion. Run them yourself, or point your own agent at them over MCP and have it report back against your codebase.

01 · 4 checks

Own your context

Own your context, rent the model.

  • Can a client take everything they own in one call?

    High
    Clears it
    One export path that enumerates every tenant-scoped table, with a test that derives the table list from the schema rather than restating it.
    Fails it
    An export covering the tables someone remembered, which silently stops covering the ones added later.
    Where to look
    the export routeany export testcolumns named tenant_id / workspace_id / account_id
  • Do the vectors live in a database you control?

    High
    Clears it
    Embeddings stored in your own store, re-derivable from source documents you also hold.
    Fails it
    The only copy of the index lives inside a vendor, so leaving means re-ingesting everything from scratch, if you still have it.
    Where to look
    the vector store clientthe ingestion pathwhether the source text is retained after embedding
  • Is the model behind one seam, or wired into call sites?

    Medium
    Clears it
    A single provider module and model ids as typed constants, so swapping a model is one edit.
    Fails it
    A vendor SDK imported directly wherever generation happens, with model ids as loose strings.
    Where to look
    imports of provider SDKs across the treewhere model id strings appear
  • Does delete reach the things that are not rows?

    Critical
    Clears it
    One teardown primitive that destroys external resources before the database cascade, reused by every delete path.
    Fails it
    A foreign-key cascade treated as the whole deletion contract, while provisioned projects, repositories, subscriptions and VMs keep running.
    Where to look
    delete servicesany column holding an external id or refwhat happens to those on delete

02 · 5 checks

Grounded or it doesn't answer

If it can't cite it, it shouldn't say it.

  • Does the answering call see only what was retrieved?

    Critical
    Clears it
    Retrieval runs first and its output is the only source material in the prompt.
    Fails it
    A system prompt full of background the model can blend with, so you cannot tell a retrieved fact from a remembered one.
    Where to look
    the prompt assembly for the answer pathwhat else is concatenated alongside the retrieved chunks
  • Are citations built from retrieved ids, or written by the model?

    Critical
    Clears it
    The renderer resolves citations against the chunks actually retrieved, and a citation that resolves to nothing is caught.
    Fails it
    The model emits citation markers as free text and the UI renders them, so a fabricated source looks identical to a real one.
    Where to look
    how citations are parsed and renderedany check that a cited id was in the retrieved set
  • Is refusal enforced in code, or only requested in the prompt?

    Critical
    Clears it
    A predicate decides refusal from the retrieval result, independent of what the model chose to say.
    Fails it
    The instruction says to refuse when unsure, and nothing verifies that it did.
    Where to look
    a grounding or refusal predicatewhether the answer path can return an ungrounded answer at all
  • Does the interface claim a refusal only when one happened?

    High
    Clears it
    What the user is told is derived from the same signal that is recorded, and the showable claim is never weaker than the recorded one.
    Fails it
    A reassuring card saying the system stopped rather than guess, rendered over an answer it actually generated.
    Where to look
    the refusal or trust componentthe field it readswhether that field is the recorded one
  • Does the eval set contain questions that must NOT be answered?

    High
    Clears it
    A golden set with explicit refusal cases, run when retrieval, chunking, the model or the grounding prompt changes.
    Fails it
    An eval that only measures recall, which a model that answers everything confidently passes.
    Where to look
    the eval setthe ratio of answerable to unanswerable cases

03 · 5 checks

Untrusted content is data, not instructions

Retrieved and user content is data the model reads, never orders it follows.

  • Is retrieved and user content marked as untrusted where it enters the prompt?

    High
    Clears it
    Every untrusted block is delimited and labelled as reference material that is never an instruction.
    Fails it
    Document text pasted into the prompt indistinguishable from your own instructions.
    Where to look
    how retrieved chunks are formatted into the promptany untrusted marker
  • Can retrieved content reach a tool call?

    Critical
    Clears it
    The call that answers from untrusted context has no tools bound to it, so there is nothing for injected text to trigger.
    Fails it
    One agent loop with both retrieval output and write tools available, where a crafted document can call them.
    Where to look
    the tool list on the answer callwhether retrieval and tool use share a loop
  • Does the component that reads hostile input have tools at all?

    Critical
    Clears it
    Anything parsing inbound email, uploads or third-party content returns structured data and can call nothing.
    Fails it
    An extractor with tool access, so the content it parses decides what it does.
    Where to look
    inbound email, upload and webhook processingthe tool set on each
  • Are tool arguments validated server-side before they execute?

    High
    Clears it
    Every tool argument is schema-checked at the boundary, with bounds shared with the API schema rather than restated.
    Fails it
    Arguments trusted because the model produced them, so a prompted id reaches a query.
    Where to look
    tool definitionswhere their inputs are parsedwhether bounds are duplicated
  • If the agent runs commands, is the set allowed or merely filtered?

    Critical
    Clears it
    An explicit allowlist of permitted commands, denying everything else by default.
    Fails it
    A denylist of dangerous patterns, which is a guess about every command you did not think of.
    Where to look
    any command execution pathhow the permitted set is expressed

04 · 5 checks

The agent proposes, a human decides

Autonomy stops at anything you can't undo.

  • Does agent-authored code reach the default branch directly?

    Critical
    Clears it
    Changes land on a branch as a pull request a human reviews. The default branch is never written by the agent.
    Fails it
    Commits pushed straight to main, where the review step is whatever the reviewer notices afterwards.
    Where to look
    the push pathwhich ref it targetswhether a PR is opened
  • Is approval required before anything you cannot undo?

    Critical
    Clears it
    Irreversible actions block on explicit human approval. Reversible ones do not, so the gate keeps its meaning.
    Fails it
    Either no gate at all, or a gate on everything, which trains people to click through it.
    Where to look
    the approval flowwhich actions require itwhich do not
  • Is the diff that was approved provably the diff that lands?

    High
    Clears it
    The base is compared between approval and push, and an ambiguous or truncated comparison blocks rather than proceeds.
    Fails it
    Approval on a diff computed against a base that has since moved, so the merge contains work nobody reviewed.
    Where to look
    what happens between approval and pushany base comparisonwhat it does when it cannot tell
  • Is the record of who did what append-only and attributed?

    High
    Clears it
    An audit trail nothing updates or deletes, recording actor, action and time, including the operator's own access.
    Fails it
    A log with an update path, or one that records the client's actions but not the vendor's.
    Where to look
    the audit tableits grantswhether operator access is recorded too
  • Is agent-written code scanned for secrets before it is pushed?

    High
    Clears it
    A scan runs before the push, and a hit blocks it rather than annotating it.
    Fails it
    Scanning after the fact, or not at all, so a generated example key becomes a real commit.
    Where to look
    any secret scanwhether it gates the push or reports on it

05 · 5 checks

Two layers or it's one bug from a breach

Auth in the app is not isolation. Enforce it at the database too.

  • Does every content row carry a tenant id, and does every query filter on it?

    Critical
    Clears it
    One tenant column on every content table, and services that filter by the RESOLVED tenant on every read and write.
    Fails it
    A table that joins its way to a tenant, so one missing join is a cross-tenant read.
    Where to look
    the schema for tenant columnsservice queries for the filterany table lacking one
  • Is there a second layer under the application checks?

    Critical
    Clears it
    Row-level security on tenant tables with policies, and closed grants on anything holding tokens, money or infrastructure state.
    Fails it
    Application checks only, so one missing filter is a breach rather than a bug.
    Where to look
    which tables have RLS enabledwhich have policieswhich grants exist for the anonymous and authenticated roles
  • Where does the tenant id come from?

    Critical
    Clears it
    Resolved from the authenticated user on every request. A tenant id in a request body is never trusted.
    Fails it
    An id read from the body or a query parameter, which makes every endpoint a tenant switcher.
    Where to look
    route handlerswhat the tenant id is derived fromany body field naming a tenant
  • Does a foreign id return 404 or 403?

    Medium
    Clears it
    404, so the response does not confirm that the resource exists.
    Fails it
    403 on a resource belonging to someone else, which is an existence oracle you can enumerate.
    Where to look
    the not-found path in the tenant gatewhat status a foreign id produces
  • Can a service-role or admin key reach the browser?

    Critical
    Clears it
    Privileged clients are constructed only in server modules, and the build would fail if one were imported into client code.
    Fails it
    A single shared client, or a key in a variable the bundler exposes.
    Where to look
    where the privileged client is builtwhether those modules are server-onlypublic environment variable names

06 · 5 checks

One boundary: API-first, validated, typed

One server boundary that validates everything. No business logic in the client.

  • Is every input schema-validated at one server boundary?

    High
    Clears it
    One validation helper and schemas that live with the domain, imported by every surface that accepts that input.
    Fails it
    Hand-rolled checks per handler, and bounds restated in each tool and form, which drift apart.
    Where to look
    the validation helperwhere schemas are definedany handler parsing input itself
  • Does authorization happen before any data work?

    Critical
    Clears it
    The tenant gate is the first statement in the handler, returning early on failure.
    Fails it
    Validation, lookups or writes before the gate, so an unauthorized request still moves the database.
    Where to look
    the first lines of each handleranything before the gate
  • Can the browser write to the database directly?

    Critical
    Clears it
    All mutations go through the server boundary. The client holds no write credential.
    Fails it
    A public key with insert rights, where correctness depends on rules written somewhere else.
    Where to look
    client-side database callsthe grants on the public role
  • Do handlers state their return type?

    Medium
    Clears it
    An explicit return annotation on every handler, so a drift in what it returns is a compile error.
    Fails it
    Inferred return types, which the compiler accepts happily while the response shape changes underneath.
    Where to look
    handler signaturesany without an annotation
  • Do route bodies do the work?

    Medium
    Clears it
    Handlers gate, validate and shape the response. Services own the work and the tenant scoping.
    Fails it
    Database work inside handlers, or services returning HTTP responses, which puts the security boundary in two places.
    Where to look
    handler lengthdatabase calls inside handlersHTTP types imported into services

07 · 6 checks

Bounded by design

An agent with no limits is a bill and an outage waiting to happen.

  • What stops an agent loop that does not converge?

    High
    Clears it
    Explicit step, retry and cost ceilings, enforced by the runtime rather than by the prompt.
    Fails it
    A loop that runs until the model decides to stop, which is a bill with no upper bound.
    Where to look
    the agent loopmax step and retry settingsany per-run cost ceiling
  • Does every outbound call carry a timeout?

    Medium
    Clears it
    A timeout on every external call, chosen deliberately rather than inherited from a default.
    Fails it
    A hung provider holding a request open until the platform kills it, taking the work with it.
    Where to look
    fetch and SDK calls to third partiestimeout or abort signals
  • Is a retried unit of work charged and applied once?

    Critical
    Clears it
    A deterministic key derived from the CONTENT of the work, so a resumed or re-delivered job collapses onto the same record.
    Fails it
    A key derived from position, a cursor or a parent id, which produces a fresh key on resume and bills the same work twice.
    Where to look
    idempotency keyswhat each is derived fromwhether a resumed batch would produce the same one
  • What happens at zero balance, and what happens when the balance cannot be read?

    Critical
    Clears it
    A hard, explicit block at zero, and a read failure that fails closed rather than assuming credit.
    Fails it
    Silent degradation, or a database error read as no balance found and treated as fine.
    Where to look
    the spend gateevery balance readwhat each does with an error result
  • What recovers work that dies between two states?

    High
    Clears it
    Every non-terminal status has a sweeper that does not depend on the in-request error handler, because a hard kill never runs it.
    Fails it
    A status moved to terminal only inside a catch block, so an out-of-memory kill leaves it stuck forever.
    Where to look
    every non-terminal status valuewhat sets it to terminalwhether anything sweeps it
  • Are unbounded reads capped, and does the surface admit when it truncated?

    Medium
    Clears it
    A cap on any query that grows with the data, and a surface that states it is showing a subset.
    Fails it
    A page that loads everything, or worse, one that caps silently and renders the capped number as the total.
    Where to look
    queries with no limitoperator pageswhether a truncated count is labelled

08 · 5 checks

You can tell when it's wrong

If you can't trace a bad answer back to its cause, you can't trust the good ones.

  • Is there a catch block that discards its error?

    High
    Clears it
    Every failure path logs with enough context to identify the tenant and the operation.
    Fails it
    An empty catch, or one that swallows and returns a default, which is a failure nobody will ever see.
    Where to look
    catch blockswhich ones logwhich return a fallback quietly
  • Does the data layer throw on failure, or resolve with an error you have to read?

    Critical
    Clears it
    If it resolves, every write reads the error and handles it, and webhook paths rethrow so the provider re-delivers.
    Fails it
    An awaited write whose result is discarded, which is a fully silent data loss on a constraint or a timeout.
    Where to look
    awaited writes with no destructured errorwebhook handlerswhether a failure there returns success
  • What does a failed read on a billing path return?

    Critical
    Clears it
    It throws. A transient database error is never allowed to read as zero, empty or not found on a money path.
    Fails it
    A wallet or subscription read that swallows its error, so a blip becomes a wrong charge or a free tier granted forever.
    Where to look
    reads on wallet, subscription, ledger and customer recordswhat each does with an error
  • Where do the loud signals actually go?

    High
    Clears it
    Logs reach a drain someone can query after the fact, so a mitigation whose enforcement is that it logs loudly is real.
    Fails it
    Structured error events written to standard output with no drain, which is detection nobody receives.
    Where to look
    the logger implementationwhether a drain or sink is configured
  • Can you get from a wrong answer back to what produced it?

    High
    Clears it
    A record per generation carrying what was retrieved, which model ran, what it cost, and whether it was grounded.
    Fails it
    A response with no trace, so the only debugging tool is asking again and hoping it happens twice.
    Where to look
    the per-generation recordwhether retrieval context is recoverable from it

Run it yourself

Take this with you

Run the hardening recipe against your repository

Point a coding agent at your repository and run this. It is read-only: it maps the stack first, runs every check with one subagent per principle, has a second agent refute each gap, and writes one report. It changes no code, and a check it cannot settle is recorded as a question rather than a failure.

  1. 00
    How to run thisRead-only, end to end, as an orchestrator: one subagent per principle, a different agent to refute, your own context kept for the map and the report.
  2. 0A
    The honesty ruleFour verdicts, and unknown is not a gap. A check it could not settle becomes a question the owner can answer in a sentence, never a mark against the system.
  3. 01
    Know the repository firstMap the stack, what each dependency decides, the shape from real files, and the instruction layer. Then translate every check's Look into this repository's own paths.
  4. 02
    Run the checksOne subagent per principle. Every check returns a verdict, the evidence as path and line, one sentence of why, and what it did not read.
  5. 03
    RefuteA different agent re-reads the evidence behind every gap and every critical verdict, and returns confirmed, refuted or downgraded.
  6. 04
    The reportOrdered by what it costs when it is wrong, with unknowns as questions, what went unread, and the three things to fix first.

The checks travel with the prompt, so it is long. Paste this line instead and your agent fetches the whole thing itself. The copy and download buttons put the text in your repository, which works too.

Read https://alicantorun.com/lab/hardening-recipe.md in full and follow it exactly, against this repository. Do not summarise it: run the checks and write the report.

Run it in Claude Code on Opus 5 with maximum effort, in a fresh session. It spawns a subagent per principle and reads a lot of files; a weaker setting answers from the framework's reputation instead of from your code.

The whole prompt

Run the hardening recipe against this repository

You are auditing a codebase you may not have written, against the method Alican Torun uses to decide whether an AI-built system can be trusted in production: 40 checks across 8 principles, each one answerable by reading code. You change nothing. This is a read-only pass that ends in one written report.

Read it as invariants, not as a stack. A check asks what must be true, never which library provides it, so translate every Look into whatever this repository actually uses. Where the mechanism a check assumes does not exist here at all, the check is not applicable with the reason, never a gap. These were earned on TypeScript, Node and Postgres, and they hold anywhere the same property has to hold.

How to run this

You run inside Claude Code or a harness like it, on your own, end to end: map, run the checks, verify, report. You do not ask which checks matter; severity and evidence decide that. Work as an orchestrator rather than a single reader: one subagent per principle, each handed the map and returning findings in one shape, and a different agent instructed to refute every gap before it reaches the summary. Spend your own context on the map and the report, and delegate the reading.

The honesty rule, which is load-bearing

Every check gets one verdict:

  • strong: the code does this, and you read the file that proves it.
  • partial: it holds on some paths and not others. Name both.
  • gap: the code does not do this, and you read enough to say so.
  • unknown: you could not settle it from what you read.

Unknown is not a gap. Absence of evidence is a question, never a mark against the system, so an unknown becomes a sharp question the owner can answer in one sentence. A check you skipped is unknown, not strong. Guessing in either direction makes the whole report worthless, because the reader can no longer tell which verdicts were earned.

Absolute constraints

  • Change no file except the report. No commits, no installs, no migrations, no deploys, no formatting and no fixes: what is worth fixing gets written down, not done.
  • Read no secret value. Check that a variable exists, never what it holds.
  • Run nothing that writes: no seeds, no scripts against a database, no call that spends money with a real provider key. Reading files, searching the tree, and the repository's own read-only commands are enough.
  • Cite what you read. A verdict with no file and line behind it is an opinion.
  • Report what you could not verify, and never soften a real finding.

Phase 1: know the repository first

Grep matches spelling, not meaning: a check written against one client finds nothing in a repository that uses another, and zero hits then reads as clean. So map before you judge, fanned out, and write the map at the top of docs/HARDENING-AUDIT.md:

  • The stack: framework, HTTP layer, database client or ORM, auth, validation, test runner, CI and deployment target, read from the manifest and the lockfile rather than the README.
  • What each dependency decides: does the database client throw or resolve its errors, is there a mutation path that bypasses the API, does the database authorise rows or connect as one pooled role, and where is the model actually called.
  • The shape, from real files: one route, one service, one schema, one test, one model call.
  • The instruction layer: AGENTS.md, CLAUDE.md, rules, agent definitions, hooks, cursor and copilot files, ADRs. Claims to check, not truth.

Then translate every check's Look into this repository's own paths and symbols. A check whose mechanism does not exist here (no agent, no tenancy, no money path) is not applicable with the reason, never a gap.

Phase 2: run the checks

One subagent per principle, in parallel. For every check it returns the id, the verdict, the evidence as path:line with at most three quoted lines, one sentence of why, and what it did not read. Read the files: do not answer from a framework's reputation, from a dependency's documentation, or from what the code is probably doing.

The checks

1. Own your context

Own your context, rent the model. The bar: The client owns their data and embeddings, exportable and portable, so the model is a swappable part.

  • ownership.export-is-complete (high) Can a client take everything they own in one call?
    • Clears: One export path that enumerates every tenant-scoped table, with a test that derives the table list from the schema rather than restating it.
    • Fails: An export covering the tables someone remembered, which silently stops covering the ones added later.
    • Look: the export route; any export test; columns named tenant_id / workspace_id / account_id
  • ownership.embeddings-are-yours (high) Do the vectors live in a database you control?
    • Clears: Embeddings stored in your own store, re-derivable from source documents you also hold.
    • Fails: The only copy of the index lives inside a vendor, so leaving means re-ingesting everything from scratch, if you still have it.
    • Look: the vector store client; the ingestion path; whether the source text is retained after embedding
  • ownership.model-is-swappable (medium) Is the model behind one seam, or wired into call sites?
    • Clears: A single provider module and model ids as typed constants, so swapping a model is one edit.
    • Fails: A vendor SDK imported directly wherever generation happens, with model ids as loose strings.
    • Look: imports of provider SDKs across the tree; where model id strings appear
  • ownership.deletion-is-real (critical) Does delete reach the things that are not rows?
    • Clears: One teardown primitive that destroys external resources before the database cascade, reused by every delete path.
    • Fails: A foreign-key cascade treated as the whole deletion contract, while provisioned projects, repositories, subscriptions and VMs keep running.
    • Look: delete services; any column holding an external id or ref; what happens to those on delete

2. Grounded or it doesn't answer

If it can't cite it, it shouldn't say it. The bar: Answers come only from retrieved sources, are cited, and the system refuses when the answer is not there.

  • grounding.context-is-retrieved (critical) Does the answering call see only what was retrieved?
    • Clears: Retrieval runs first and its output is the only source material in the prompt.
    • Fails: A system prompt full of background the model can blend with, so you cannot tell a retrieved fact from a remembered one.
    • Look: the prompt assembly for the answer path; what else is concatenated alongside the retrieved chunks
  • grounding.citations-are-derived (critical) Are citations built from retrieved ids, or written by the model?
    • Clears: The renderer resolves citations against the chunks actually retrieved, and a citation that resolves to nothing is caught.
    • Fails: The model emits citation markers as free text and the UI renders them, so a fabricated source looks identical to a real one.
    • Look: how citations are parsed and rendered; any check that a cited id was in the retrieved set
  • grounding.refusal-is-code (critical) Is refusal enforced in code, or only requested in the prompt?
    • Clears: A predicate decides refusal from the retrieval result, independent of what the model chose to say.
    • Fails: The instruction says to refuse when unsure, and nothing verifies that it did.
    • Look: a grounding or refusal predicate; whether the answer path can return an ungrounded answer at all
  • grounding.the-claim-is-checked (high) Does the interface claim a refusal only when one happened?
    • Clears: What the user is told is derived from the same signal that is recorded, and the showable claim is never weaker than the recorded one.
    • Fails: A reassuring card saying the system stopped rather than guess, rendered over an answer it actually generated.
    • Look: the refusal or trust component; the field it reads; whether that field is the recorded one
  • grounding.evals-cover-refusal (high) Does the eval set contain questions that must NOT be answered?
    • Clears: A golden set with explicit refusal cases, run when retrieval, chunking, the model or the grounding prompt changes.
    • Fails: An eval that only measures recall, which a model that answers everything confidently passes.
    • Look: the eval set; the ratio of answerable to unanswerable cases

3. Untrusted content is data, not instructions

Retrieved and user content is data the model reads, never orders it follows. The bar: A hard boundary: target text, documents, and tool output cannot trigger tools or actions.

  • injection.untrusted-is-fenced (high) Is retrieved and user content marked as untrusted where it enters the prompt?
    • Clears: Every untrusted block is delimited and labelled as reference material that is never an instruction.
    • Fails: Document text pasted into the prompt indistinguishable from your own instructions.
    • Look: how retrieved chunks are formatted into the prompt; any untrusted marker
  • injection.no-tools-on-the-grounded-answer (critical) Can retrieved content reach a tool call?
    • Clears: The call that answers from untrusted context has no tools bound to it, so there is nothing for injected text to trigger.
    • Fails: One agent loop with both retrieval output and write tools available, where a crafted document can call them.
    • Look: the tool list on the answer call; whether retrieval and tool use share a loop
  • injection.extractors-are-tool-free (critical) Does the component that reads hostile input have tools at all?
    • Clears: Anything parsing inbound email, uploads or third-party content returns structured data and can call nothing.
    • Fails: An extractor with tool access, so the content it parses decides what it does.
    • Look: inbound email, upload and webhook processing; the tool set on each
  • injection.tool-args-are-validated (high) Are tool arguments validated server-side before they execute?
    • Clears: Every tool argument is schema-checked at the boundary, with bounds shared with the API schema rather than restated.
    • Fails: Arguments trusted because the model produced them, so a prompted id reaches a query.
    • Look: tool definitions; where their inputs are parsed; whether bounds are duplicated
  • injection.commands-are-allowlisted (critical) If the agent runs commands, is the set allowed or merely filtered?
    • Clears: An explicit allowlist of permitted commands, denying everything else by default.
    • Fails: A denylist of dangerous patterns, which is a guess about every command you did not think of.
    • Look: any command execution path; how the permitted set is expressed

4. The agent proposes, a human decides

Autonomy stops at anything you can't undo. The bar: Destructive or irreversible actions need explicit approval, and every change is attributed and reversible. Audit trails are append-only.

  • reversibility.writes-land-on-a-branch (critical) Does agent-authored code reach the default branch directly?
    • Clears: Changes land on a branch as a pull request a human reviews. The default branch is never written by the agent.
    • Fails: Commits pushed straight to main, where the review step is whatever the reviewer notices afterwards.
    • Look: the push path; which ref it targets; whether a PR is opened
  • reversibility.approval-precedes-the-irreversible (critical) Is approval required before anything you cannot undo?
    • Clears: Irreversible actions block on explicit human approval. Reversible ones do not, so the gate keeps its meaning.
    • Fails: Either no gate at all, or a gate on everything, which trains people to click through it.
    • Look: the approval flow; which actions require it; which do not
  • reversibility.approved-is-what-lands (high) Is the diff that was approved provably the diff that lands?
    • Clears: The base is compared between approval and push, and an ambiguous or truncated comparison blocks rather than proceeds.
    • Fails: Approval on a diff computed against a base that has since moved, so the merge contains work nobody reviewed.
    • Look: what happens between approval and push; any base comparison; what it does when it cannot tell
  • reversibility.trail-is-append-only (high) Is the record of who did what append-only and attributed?
    • Clears: An audit trail nothing updates or deletes, recording actor, action and time, including the operator's own access.
    • Fails: A log with an update path, or one that records the client's actions but not the vendor's.
    • Look: the audit table; its grants; whether operator access is recorded too
  • reversibility.secrets-are-scanned-before-they-leave (high) Is agent-written code scanned for secrets before it is pushed?
    • Clears: A scan runs before the push, and a hit blocks it rather than annotating it.
    • Fails: Scanning after the fact, or not at all, so a generated example key becomes a real commit.
    • Look: any secret scan; whether it gates the push or reports on it

5. Two layers or it's one bug from a breach

Auth in the app is not isolation. Enforce it at the database too. The bar: Every read and write is tenant-scoped, with row-level security and least-privilege grants underneath the app checks.

  • isolation.every-row-is-scoped (critical) Does every content row carry a tenant id, and does every query filter on it?
    • Clears: One tenant column on every content table, and services that filter by the RESOLVED tenant on every read and write.
    • Fails: A table that joins its way to a tenant, so one missing join is a cross-tenant read.
    • Look: the schema for tenant columns; service queries for the filter; any table lacking one
  • isolation.the-database-enforces-it-too (critical) Is there a second layer under the application checks?
    • Clears: Row-level security on tenant tables with policies, and closed grants on anything holding tokens, money or infrastructure state.
    • Fails: Application checks only, so one missing filter is a breach rather than a bug.
    • Look: which tables have RLS enabled; which have policies; which grants exist for the anonymous and authenticated roles
  • isolation.tenant-comes-from-the-session (critical) Where does the tenant id come from?
    • Clears: Resolved from the authenticated user on every request. A tenant id in a request body is never trusted.
    • Fails: An id read from the body or a query parameter, which makes every endpoint a tenant switcher.
    • Look: route handlers; what the tenant id is derived from; any body field naming a tenant
  • isolation.a-miss-is-a-404 (medium) Does a foreign id return 404 or 403?
    • Clears: 404, so the response does not confirm that the resource exists.
    • Fails: 403 on a resource belonging to someone else, which is an existence oracle you can enumerate.
    • Look: the not-found path in the tenant gate; what status a foreign id produces
  • isolation.privileged-keys-stay-server-side (critical) Can a service-role or admin key reach the browser?
    • Clears: Privileged clients are constructed only in server modules, and the build would fail if one were imported into client code.
    • Fails: A single shared client, or a key in a variable the bundler exposes.
    • Look: where the privileged client is built; whether those modules are server-only; public environment variable names

6. One boundary: API-first, validated, typed

One server boundary that validates everything. No business logic in the client. The bar: Every input is schema-validated at a single API boundary; the backend is framework-agnostic; no mutations from the client.

  • boundary.one-validated-entry (high) Is every input schema-validated at one server boundary?
    • Clears: One validation helper and schemas that live with the domain, imported by every surface that accepts that input.
    • Fails: Hand-rolled checks per handler, and bounds restated in each tool and form, which drift apart.
    • Look: the validation helper; where schemas are defined; any handler parsing input itself
  • boundary.the-gate-runs-first (critical) Does authorization happen before any data work?
    • Clears: The tenant gate is the first statement in the handler, returning early on failure.
    • Fails: Validation, lookups or writes before the gate, so an unauthorized request still moves the database.
    • Look: the first lines of each handler; anything before the gate
  • boundary.the-client-does-not-write (critical) Can the browser write to the database directly?
    • Clears: All mutations go through the server boundary. The client holds no write credential.
    • Fails: A public key with insert rights, where correctness depends on rules written somewhere else.
    • Look: client-side database calls; the grants on the public role
  • boundary.handlers-declare-their-contract (medium) Do handlers state their return type?
    • Clears: An explicit return annotation on every handler, so a drift in what it returns is a compile error.
    • Fails: Inferred return types, which the compiler accepts happily while the response shape changes underneath.
    • Look: handler signatures; any without an annotation
  • boundary.logic-lives-in-services (medium) Do route bodies do the work?
    • Clears: Handlers gate, validate and shape the response. Services own the work and the tenant scoping.
    • Fails: Database work inside handlers, or services returning HTTP responses, which puts the security boundary in two places.
    • Look: handler length; database calls inside handlers; HTTP types imported into services

7. Bounded by design

An agent with no limits is a bill and an outage waiting to happen. The bar: Step, retry, and cost caps; timeouts on external calls; idempotent and compensating side effects; fail closed.

  • bounded.loops-have-caps (high) What stops an agent loop that does not converge?
    • Clears: Explicit step, retry and cost ceilings, enforced by the runtime rather than by the prompt.
    • Fails: A loop that runs until the model decides to stop, which is a bill with no upper bound.
    • Look: the agent loop; max step and retry settings; any per-run cost ceiling
  • bounded.external-calls-time-out (medium) Does every outbound call carry a timeout?
    • Clears: A timeout on every external call, chosen deliberately rather than inherited from a default.
    • Fails: A hung provider holding a request open until the platform kills it, taking the work with it.
    • Look: fetch and SDK calls to third parties; timeout or abort signals
  • bounded.retries-are-idempotent (critical) Is a retried unit of work charged and applied once?
    • Clears: A deterministic key derived from the CONTENT of the work, so a resumed or re-delivered job collapses onto the same record.
    • Fails: A key derived from position, a cursor or a parent id, which produces a fresh key on resume and bills the same work twice.
    • Look: idempotency keys; what each is derived from; whether a resumed batch would produce the same one
  • bounded.spend-fails-closed (critical) What happens at zero balance, and what happens when the balance cannot be read?
    • Clears: A hard, explicit block at zero, and a read failure that fails closed rather than assuming credit.
    • Fails: Silent degradation, or a database error read as no balance found and treated as fine.
    • Look: the spend gate; every balance read; what each does with an error result
  • bounded.stuck-work-is-reaped (high) What recovers work that dies between two states?
    • Clears: Every non-terminal status has a sweeper that does not depend on the in-request error handler, because a hard kill never runs it.
    • Fails: A status moved to terminal only inside a catch block, so an out-of-memory kill leaves it stuck forever.
    • Look: every non-terminal status value; what sets it to terminal; whether anything sweeps it
  • bounded.scans-are-bounded-and-say-so (medium) Are unbounded reads capped, and does the surface admit when it truncated?
    • Clears: A cap on any query that grows with the data, and a surface that states it is showing a subset.
    • Fails: A page that loads everything, or worse, one that caps silently and renders the capped number as the total.
    • Look: queries with no limit; operator pages; whether a truncated count is labelled

8. You can tell when it's wrong

If you can't trace a bad answer back to its cause, you can't trust the good ones. The bar: Structured logs on success and failure with context, a trace from a finding back through the steps, and evals. Never a silent catch.

  • observability.no-silent-catch (high) Is there a catch block that discards its error?
    • Clears: Every failure path logs with enough context to identify the tenant and the operation.
    • Fails: An empty catch, or one that swallows and returns a default, which is a failure nobody will ever see.
    • Look: catch blocks; which ones log; which return a fallback quietly
  • observability.writes-check-their-result (critical) Does the data layer throw on failure, or resolve with an error you have to read?
    • Clears: If it resolves, every write reads the error and handles it, and webhook paths rethrow so the provider re-delivers.
    • Fails: An awaited write whose result is discarded, which is a fully silent data loss on a constraint or a timeout.
    • Look: awaited writes with no destructured error; webhook handlers; whether a failure there returns success
  • observability.money-reads-fail-closed (critical) What does a failed read on a billing path return?
    • Clears: It throws. A transient database error is never allowed to read as zero, empty or not found on a money path.
    • Fails: A wallet or subscription read that swallows its error, so a blip becomes a wrong charge or a free tier granted forever.
    • Look: reads on wallet, subscription, ledger and customer records; what each does with an error
  • observability.signals-reach-somebody (high) Where do the loud signals actually go?
    • Clears: Logs reach a drain someone can query after the fact, so a mitigation whose enforcement is that it logs loudly is real.
    • Fails: Structured error events written to standard output with no drain, which is detection nobody receives.
    • Look: the logger implementation; whether a drain or sink is configured
  • observability.a-bad-answer-is-traceable (high) Can you get from a wrong answer back to what produced it?
    • Clears: A record per generation carrying what was retrieved, which model ran, what it cost, and whether it was grounded.
    • Fails: A response with no trace, so the only debugging tool is asking again and hoping it happens twice.
    • Look: the per-generation record; whether retrieval context is recoverable from it

Phase 3: refute

A different agent re-reads the evidence behind every gap and every critical verdict, and returns confirmed, refuted or downgraded with its reason. An author cannot check their own finding, for the same reason nobody proofreads their own writing well. Only confirmed findings reach the summary; refuted ones stay in the report, marked, so the next reader does not rediscover them.

Phase 4: the report

Write docs/HARDENING-AUDIT.md: the map, then the findings, then the questions.

  • Lead with what it costs when it is wrong, not with what is easy to fix: critical first, then high, then the rest.
  • One line per check (id, verdict, evidence, one sentence), grouped by principle, with a one-line verdict for each principle.
  • A section of unknowns as questions, each phrased so the owner can answer it in a sentence.
  • A section of what you did not read, and why.
  • Close with the three things you would fix first, in order, and what each costs if it is left.

State plainly what this is: a point-in-time read of the code you could see. It is not a penetration test, not a compliance certification, and not a guarantee.