{"slug": "show-hn-paranoia-get-a-cold-adversarial-review-of-your-code", "title": "Show HN: Paranoia – Get a cold, adversarial review of your code", "summary": "Paranoia, a new open-source tool from developer subvertnormality, provides adversarial code reviews by running one frontier coding agent against another: it integrates with Claude Code to have Codex review code, or with Codex to have Claude Code review, using a local MCP server. The tool, which requires Python 3.11+, git, and a subscription to the reviewing agent's CLI, offers five tools including critique_branch, critique_plan, query, rebut, and arbitrate, and includes a convergence loop to iterate reviews until issues are resolved.", "body_md": "Get a cold, adversarial review of your code, your plans, and your decisions from\nthe *other* frontier coding agent — running locally, on its own subscription, with\nfull read access to your repository.\n\nInstall it into Claude Code and reviews are performed by Codex. Install it into Codex and reviews are performed by Claude Code. paranoia-local is the MCP server between them: it builds the prompt, runs the other agent read-only, and returns a structured critique.\n\n```\n┌──────────────┐   \"paranoia: critique this branch\"   ┌───────────────┐\n│  Claude Code │ ───────────────────────────────────► │ paranoia-local│\n│  (your work) │                                      │  (MCP, local) │\n└──────────────┘                                      └───────┬───────┘\n                                                              │ codex exec (read-only)\n                                                      ┌───────▼────────┐\n                                                      │  Codex / GPT-5 │ ← reads the repo,\n                                                      │  cold reviewer │   decides what to open\n                                                      └────────────────┘\n```\n\n**Contents** · [Quickstart](#quickstart) · [The five tools](#the-five-tools) ·\n[How reviews work: the convergence loop](#how-reviews-work-the-convergence-loop) ·\n[Tool reference](#tool-reference) · [Output reference](#output-reference) ·\n[Configuration](#configuration) · [Safety model](#safety-model) ·\n[Development](#development)\n\n**1. Prerequisites**\n\n- Python 3.11+ and\n`git`\n\non`PATH`\n\n- The reviewing agent's CLI, installed and signed in on a subscription:\n[Codex CLI](https://developers.openai.com/codex)(`codex`\n\n, ≥ 0.144)**or**[Claude Code](https://code.claude.com)(`claude`\n\n) `arbitrate`\n\nneeds**both** CLIs; the four review tools need only the other one\n\n**2. Install**\n\n```\ngit clone https://github.com/subvertnormality/paranoia-local\ncd paranoia-local\npip install -e .\n```\n\n**3. Wire it into your agent.** `--engine`\n\nnames the agent that *performs* reviews,\nwhich is the opposite one from the caller.\n\n**Into Claude Code** (reviews performed by Codex)\n\n```\nclaude mcp add paranoia -- paranoia-local --engine codex\n```\n\n**Into Codex** (reviews performed by Claude Code) — needs two extra keys\n\n```\ncodex mcp add paranoia -- paranoia-local --engine claude\n```\n\nThen edit `~/.codex/config.toml`\n\n. Codex defaults to a 60-second tool timeout and a\n10-second startup timeout; a review runs for minutes, so both must be raised or\nevery call fails:\n\n```\n[mcp_servers.paranoia]\ncommand = \"paranoia-local\"\nargs = [\"--engine\", \"claude\"]\ntool_timeout_sec = 3600\nstartup_timeout_sec = 30\n```\n\nVerify with `codex mcp get paranoia`\n\n.\n\n**4. Ask for a review.**\n\n\"Use paranoia to critique this branch against main. Intent: add overdraft protection to\n\n`withdraw()`\n\n.\"\n\nYour agent calls:\n\n```\n{\n  \"name\": \"critique_branch\",\n  \"arguments\": {\n    \"repo_path\": \"/Users/you/Work/my-project\",\n    \"base_ref\": \"main\",\n    \"round\": 1,\n    \"diff_intent\": \"Add overdraft protection to withdraw().\"\n  }\n}\n```\n\nYou get back a five-section critique with severity-tagged findings, and a computed\n`CONVERGENCE:`\n\ntrailer telling you whether the loop may stop.\n\n| Tool | Use it to | Needs |\n|---|---|---|\n`critique_branch` |\n\n`repo_path`\n\n`critique_plan`\n\n`repo_path`\n\n+ `plan_text`\n\n| `plan_path`\n\n`query`\n\n`question`\n\n`rebut`\n\n`session_ref`\n\nfrom that review`arbitrate`\n\n**both** vendors independently`repo_path`\n\n, `decision`\n\n, `options`\n\n, `stakes`\n\nEvery review returns a `session_ref`\n\nin its footer. Pass it to `rebut`\n\nto reopen\nthat exact reviewer session.\n\nA single review is rarely the end of it. You review, you fix, you review again.\nparanoia-local models that as a **convergence loop**, and gives you four controls\nover it plus one computed signal that tells you when to stop.\n\n```\nround 1 ──► review ──► fix ──► round 2 ──► review ──► fix ──► round 3 ──► CONVERGED\n             │                              │                              +\n             └── already_raised ────────────┴── already_raised ────► CONVERGENCE: NOT-BLOCKED\n```\n\nEach round is a **fresh, cold reviewer** — it has no memory of the last one. You\ncarry state forward with the arguments below.\n\nThe 1-based round number. **Increment it every round.** At `round >= 3`\n\nthe\nreviewer reports only merge-blocking findings and withholds `[MINOR]`\n\nand\n`[OUT-OF-SCOPE]`\n\n, writing `CONVERGED`\n\nwhen none remain. This is the lever that\nmakes a loop *stop* instead of grinding through diminishing findings.\n\n`round`\n\nis required on `critique_branch`\n\nand `critique_plan`\n\nunless you pass\n`class_closure: false`\n\n.\n\nThe real deployment context, threat model, and scale the work operates in:\n\n```\n\"stakes\": \"Internal booking API, single team, authenticated first-party callers, ~1k req/min.\"\n```\n\nThe reviewer treats it as the **boundary of legitimate concern**. Findings that\nassume adversaries, scale, or failure modes beyond it are dropped or tagged\n`[OUT-OF-SCOPE]`\n\n, never must-fix. Omit it and the reviewer assumes a modest\ninternal tool; a review with no stakes ends with a `STAKES: unstated`\n\nline. Pass\n`stakes: \"unstated\"`\n\nto accept that reading deliberately and silence the line.\n\nSet it once per project in [ .paranoia.toml](#paranoiatoml); override per call to\ntighten it for a specific surface.\n\nOne-line, `file:line`\n\n-cited claims you have already accepted from earlier rounds.\nThe reviewer is told not to restate them and to hunt for what they missed. Pass\nthe claim and its citation, never the previous reviewer's prose.\n\n```\n\"already_raised\": [\n  \"withdraw() ignores pending holds — accounts.py:88\",\n  \"the overdraft test asserts the fee, not the balance — test_accounts.py:210\"\n]\n```\n\n**On by default.** A finding is usually an instance of a class: one violated\ninvariant, several sites. Class closure makes the class itself a tracked object\nthat survives the round.\n\nThe reviewer ends its review with a register block:\n\n```\n=== CLASS REGISTER ===\nCLASS: every public writer must validate its input before the first mutation\nSEVERITY: MAJOR\nPATTERN: def (create|update)_[a-z_]+\\(.*\\):\\n(?!.*validate)\nPATHSPEC: src/\n```\n\nThe server then, every round:\n\n**re-runs each registered regex itself** against the reviewed snapshot (`git grep`\n\n), and lists every surviving match to the next reviewer;**refuses to report the loop unblocked** while any`BLOCKER`\n\n/`MAJOR`\n\n/`FATAL`\n\nclass still matches;**computes the verdict in Python** and appends it as the`CONVERGENCE:`\n\ntrailer.\n\nA class closes when its predicate returns zero matches and reopens the moment it\nmatches again. `MINOR`\n\nand `OUT-OF-SCOPE`\n\nclasses are tracked but advisory — they\nnever block.\n\nWhere no regex can express the invariant, the reviewer registers a `PROCEDURE:`\n\ninstead. Those are **unmechanized**: nothing re-runs them, they are shown to every\nlater reviewer, and they close only when a reviewer explicitly writes\n`CLOSED: <class-id>`\n\n.\n\n**On critique_plan, every class is unmechanized** — a regex over prose closes as\nsoon as the wording changes, so predicates are not accepted there at all. Plan\nclosure gives you\n\n*non-forgetting plus explicit closure*, not automatic recurrence detection.\n\n**Register transitions** a reviewer can emit, besides a new class:\n\n| Record | Effect |\n|---|---|\n`CLOSED: <id>` |\nAn unmechanized class is judged closed |\n`REOPEN: <id>` |\nA closed unmechanized class is violated again |\n`RECLASSIFY: <id> <severity>` |\nCorrect a severity |\n`SUPERSEDE: <id>` + `BY:` / `WITH-PATTERN:` / `WITH-PROCEDURE:` |\nReplace a class |\n\nYou cannot emit these yourself — ask for them in `focus`\n\n, e.g. *\"class 3f2a91c4 is\nregistered MAJOR but its effect is cosmetic; reclassify it if you agree.\"*\n\nClass state lives in `~/.paranoia/lineages/<lineage>.json`\n\n.\n\n`critique_branch`\n\nderives the key from repo +`base_ref`\n\n+ reviewed branch. Pass`lineage`\n\nexplicitly when the reviewed ref is**not** a branch (a detached HEAD or a raw commit), where there is no stable key to derive.`critique_plan`\n\n**always requires** an explicit`lineage`\n\n— a plan has no branch, and nothing is derived from its text or path.\n\nThe key is used verbatim as the state filename with no namespacing, so make it\nglobally unique and mode-qualified: `myproject-42-plan`\n\nfor a plan seam,\n`myproject-42-branch`\n\nfor the branch seam of the same work. A key already used by\nthe other tool is refused rather than merged.\n\nThe stop condition is **two-part**:\n\n- the computed trailer reads\n`CONVERGENCE: NOT-BLOCKED`\n\n,**and** - the round returns\n`CONVERGED`\n\n, or only`[MINOR]`\n\n/`[OUT-OF-SCOPE]`\n\nitems.\n\nWhen the two disagree, **the trailer governs** — and says so in its own output.\n\nFor a review with no loop behind it — a design sketch, a quick second opinion —\npass `class_closure: false`\n\n. That is the single escape, and it also drops the\n`round`\n\nand `lineage`\n\nrequirements.\n\n```\n{ \"repo_path\": \"/path/to/repo\", \"plan_text\": \"…\", \"class_closure\": false }\n```\n\nWhen a registered regex matches a line that does not actually violate the invariant, exempt that exact line:\n\n```\n\"exempt\": [{\n  \"class_id\": \"3f2a91c4\",\n  \"path\": \"src/app.py\",\n  \"line\": 17,\n  \"line_text\": \"    legacy_open(state)\"\n}]\n```\n\n`line_text`\n\nmust be byte-exact including indentation. The exemption is keyed on it\nand goes void the moment that line changes, so the match resurfaces. Every\nexemption is shown to every later reviewer, with the invariant attached, so it can\nbe challenged; `unexempt`\n\ntakes the same `class_id`\n\n/`path`\n\n/`line`\n\nand revokes one.\n\nA match inside a binary blob cannot be exempted — narrow the class's `PATHSPEC`\n\ninstead.\n\nArguments marked **required** are enforced; everything else has the default shown.\n\nAdversarial review of a git branch, a committed range, or the dirty working tree.\nReturns a [five-section critique](#review-output) plus a\n[ CONVERGENCE: trailer](#class-closure-trailer).\n\n| Argument | Type | Default | Description |\n|---|---|---|---|\n`repo_path` |\nstring | required |\nAbsolute path to the git repo |\n`base_ref` |\nstring | `main` |\nBase ref for the diff |\n`head_ref` |\nstring | `HEAD` |\nHead ref to review |\n`round` |\ninteger | required unless `class_closure: false` |\n1-based round number; must be an integer ≥ 1 |\n`include_uncommitted` |\nboolean | `false` |\nReview the dirty working tree vs HEAD instead of a committed range. Runs in the live repo, not a worktree |\n`isolate` |\nboolean | `true` |\nReview inside a throwaway worktree of `head_ref` . Ignored for uncommitted reviews |\n`converge` |\nboolean | `true` |\nPre-gather a deterministic evidence packet (every touched file in full, plus the diff) and review it against an immutable materialized snapshot. Always materializes, overriding `isolate` |\n`max_packet_chars` |\ninteger | `400000` |\nCharacter budget for that packet. `already_raised` is always preserved; only file evidence is trimmed |\n`class_closure` |\nboolean | `true` |\nTrack defect classes across rounds. `false` is the one-shot mode |\n`lineage` |\nstring | derived | Explicit class-closure key. Required when the reviewed ref is not a branch |\n`exempt` / `unexempt` |\narray | — | Mark or revoke false positives of a class's regex — see\n|\n\n`stakes`\n\n`already_raised`\n\n`[]`\n\n`project_summary`\n\n`diff_intent`\n\n*supposed*to achieve. Treated as a claim to verify, never a fact to accept`focus`\n\n`engine`\n\n, `model`\n\n, `effort`\n\n, `web_search`\n\n[Common arguments](#common-arguments)`converge: false`\n\nfalls back to a legacy in-place review that has no class closure,\nso it must be paired with `class_closure: false`\n\n.\n\nAdversarial review of a plan or design document. The reviewer reads the real code\nto test every premise the plan makes about current behaviour. Returns the same\nfive sections, tagged `[FATAL]`\n\n/`[MAJOR]`\n\n/`[MINOR]`\n\n/`[OUT-OF-SCOPE]`\n\n.\n\n| Argument | Type | Default | Description |\n|---|---|---|---|\n`repo_path` |\nstring | required |\nThe repo the plan concerns |\n`plan_text` |\nstring | one of these two |\nThe plan as markdown |\n`plan_path` |\nstring | one of these two |\nAbsolute path to a markdown plan file |\n`round` |\ninteger | required unless `class_closure: false` |\n1-based round number |\n`lineage` |\nstring | required unless `class_closure: false` |\nGlobally unique, mode-qualified key. Nothing is derived |\n`class_closure` |\nboolean | `true` |\nUnmechanized classes only. `false` is the one-shot mode |\n`context` |\nstring | — | Background the reviewer needs to judge the plan fairly |\n`focus` |\nstring | — | Narrow the review to a specific concern |\n`stakes` |\nstring | — | The scope boundary |\n`already_raised` |\narray | `[]` |\nClaims already accepted from prior rounds |\n`engine` , `model` , `effort` , `web_search` |\n— | see\n|\n\n`class_closure`\n\nand `lineage`\n\nare **call arguments only** here — `.paranoia.toml`\n\nis not consulted for either.\n\nOne question, one answer. Not a full review: no five-section scaffold, lower reasoning effort by default. The reviewer reads the repo (when given one) and returns a direct answer, citations, and a stated confidence level.\n\n| Argument | Type | Default | Description |\n|---|---|---|---|\n`question` |\nstring | required |\nThe specific question to double-check |\n`repo_path` |\nstring | — | Repo to ground the answer in |\n`files` |\narray | `[]` |\n`{path, reason?}` hints to look at first — hints, not a payload; it can read anything |\n`focus` |\nstring | — | Extra framing for the question |\n`engine` , `model` , `effort` , `web_search` |\n— | `effort` defaults to `medium` |\n\nDispute one finding from a review. Resumes **that same reviewer session** with your\ncounter-evidence, so it is cheaper and higher-resolution than a fresh round. The\nreviewer replies `CONCEDE`\n\nor `HOLD`\n\nwith fresh citations.\n\n| Argument | Type | Default | Description |\n|---|---|---|---|\n`repo_path` |\nstring | required |\nSame repo the review ran against |\n`session_ref` |\nstring | required |\nFrom the prior review's footer |\n`rebuttal` |\nstring | required |\nYour counter-evidence |\n`engine` , `model` , `effort` , `web_search` |\n— | see\n|\n\nDecides between 2–4 options. Both frontier vendors judge independently and cold over one pinned snapshot, and Python computes the verdict.\n\n```\n{\n  \"repo_path\": \"/Users/you/Work/my-project\",\n  \"decision\": \"Choose the numeric type for the position-size threshold.\",\n  \"options\": [\n    {\"id\": \"opt-float\",   \"statement\": \"Store it as a float.\"},\n    {\"id\": \"opt-decimal\", \"statement\": \"Store it as a Decimal.\"}\n  ],\n  \"stakes\": \"Internal CLI, single team, threshold used only in a log line.\",\n  \"files\": [{\"path\": \"scripts/lib/registry.py\", \"reason\": \"the writer\"}]\n}\n```\n\nWhat it does, in order:\n\n**Pins one snapshot.** Each decider gets its own worktree of the same commit. Git refs and the reflog are digested before and after; if anything moved, the run returns`FAILED`\n\nrather than reporting agreement it cannot describe.**Neutralizes the framing** with an Opus agent — advocacy stripped, options equalized in detail — then has the*other*vendor attest that field by field.`stakes`\n\nis passed through verbatim, never rewritten.**Counterbalances presentation.** One decider sees canonical order, the other reversed, under opaque per-decider labels. Neither is told the other exists.**Computes the verdict.** No model adjudicates the adjudication.**On divergence**, runs one reconciliation round carrying only`path:line`\n\ncitations and bytes the server itself read — never the other model's prose — and only when there is genuinely novel evidence.\n\n| Argument | Type | Default | Description |\n|---|---|---|---|\n`repo_path` |\nstring | required |\nEvery decisive citation must be repo-verifiable |\n`decision` |\nstring | required |\nWhat is being decided (max 2500 chars) — not the evidence for it |\n`options` |\narray | required |\n2–4 mutually exclusive `{id, statement}` . Array order is irrelevant; canonical order is derived by sorting ids |\n`stakes` |\nstring | required |\nPass `\"unstated\"` to accept a fixed default reading |\n`context` |\nstring | — | Shared facts and the full specification of whatever only one option adopts (max 20000 chars) |\n`files` |\narray | `[]` |\n`{path, reason?}` starting points. Both deciders see the same list |\n`subject` |\nstring | — | Short label for the paste-ready record block |\n`clean` |\nboolean | `true` |\nRun the cleaner and its cross-vendor attestation |\n`models` |\nobject | — | `{codex?, claude?}` per-vendor overrides |\n`cleaner_model` |\nstring | `claude-opus-5` |\nOverride the cleaner model |\n`order_seed` |\nstring | — | Replay a previous run's `ORDER-SEED` to reproduce its labels and ordering |\n`retain_snapshot` |\nboolean | `false` |\nCreate `refs/paranoia/arbitrate/<stamp>` so evidence survives `git gc` |\n`effort` , `web_search` |\n— | see\n|\n\n** arbitrate has no engine or model** — it drives both vendors, so a single\noverride could only degrade it to one of them or send one vendor's model name to\nthe other CLI.\n\n**Input bounds**, checked before anything is spent:\n\n| Bound | Limit |\n|---|---|\n| option statement | 1200 chars |\n| longest ÷ shortest option | 2.0 |\n`decision` |\n2500 chars |\n`context` |\n20000 chars |\n\nThe shape that passes these naturally: put every shared fact, and the full\nspecification of whatever only one option adopts, into `context`\n\n— prefaced as\n\"the rules under consideration, if adopted\". Leave each option statement to say\nonly how much of it is adopted and what follows. ~800 chars each is typical.\n\n**Behaviour worth knowing before you rely on it:**\n\n**It only decides what the repository can settle.** A converging vote must cite a line that resolves. A decision that does not turn on repo-verifiable grounds will never return`CONVERGED`\n\n.Each decider reports whether it judges that a named human owner should be authorizing the decision. That is reported, never gated:`ADVISORY`\n\ndoes not block.`CONVERGED`\n\nwith`ADVISORY: human-owner`\n\nis still`CONVERGED`\n\n. Enforcing it is your policy.The snapshot commit is unreferenced and`SNAPSHOT`\n\nis provenance, not a replay handle.`git gc`\n\nreclaims it. The audit log holds both prompts, both replies, and the carried evidence.`retain_snapshot: true`\n\npins it behind a ref.**On divergence, only a decider that** One that held its round-1 position needs only a citation that resolves — provided its round-1 decisive citation resolved too. A holder that was never substantiated must ground in gained evidence like a mover.*moved*must ground in the carried evidence.**Bias is reduced, not eliminated.** Order counterbalancing equalizes mean rank but not higher moments for 3–4 options; attestation is a model's judgement, not a proof; and a`files`\n\nlist pointing only at evidence favouring one option biases both deciders identically.`docs/arbitration_plan.md`\n\n§2 enumerates the residuals.\n\nAccepted by the four review tools:\n\n| Argument | Values | Default |\n|---|---|---|\n`engine` |\n`codex` | `claude` |\nthe server's configured engine |\n`model` |\nany model name | the engine's strongest: `gpt-5.6-sol` / `claude-fable-5` |\n`effort` |\n`low` | `medium` | `high` |\n`high` (`query` : `medium` ) |\n`web_search` |\nboolean | `true` |\n\nEvery review returns exactly five sections, in this order:\n\n| Section | Contains |\n|---|---|\n`## What works` |\nSpecific correct decisions, cited. \"Nothing notable.\" when there are none |\n`## What doesn't work` |\nActual defects: quoted lines, failure mechanism, observable symptom. Worst first |\n`## Risks` |\nFailure modes the author didn't consider that the code is exposed to |\n`## Gaps` |\nWhat the change should do to reach its stated intent but doesn't |\n`## Improvements` |\nConcrete changes that alter the outcome under the stated stakes |\n\nEvery item in the last four sections carries exactly one severity tag:\n\n| Code review | Plan review | Meaning |\n|---|---|---|\n`[BLOCKER]` |\n`[FATAL]` |\nShips a bug / kills the plan as written |\n`[MAJOR]` |\n`[MAJOR]` |\nFix before merge / before execution |\n`[MINOR]` |\n`[MINOR]` |\nFix opportunistically |\n`[OUT-OF-SCOPE]` |\n`[OUT-OF-SCOPE]` |\nReal, but beyond the stated stakes — file separately |\n\nA finding that recurs from a tracked class is marked `[RECURRENCE <class-id>]`\n\nnext to its severity tag.\n\nThe footer carries the `session_ref`\n\nfor [ rebut](#rebut).\n\nAppended below the review whenever class closure ran:\n\n```\nLINEAGE: 9f2c1a4b0e77 (rounds recorded: 8)\nCLASS-REGISTER: parsed 1\nCLASS-CLOSURE: 1 open, 2 closed, 3 surviving matches, 0 exempt, 1 unmechanized\nCONVERGENCE: BLOCKED — 1 class(es) unclosed:\n  3f2a91c4 every public writer must validate before the first mutation (mechanized: 3 match(es))\n```\n\n| Line | Meaning |\n|---|---|\n`CONVERGENCE: NOT-BLOCKED` |\nNo blocking class is unclosed. Advisory classes may remain open |\n`CONVERGENCE: BLOCKED` |\nNamed classes are still open; any `CONVERGED` in the review above is void |\n`CLASS-REGISTER: NONE` | `parsed N` | `malformed: …` |\nWhat the reviewer's register block contained |\n`CLASS-CLOSURE-WARNING: … closed in the round it was registered` |\nThe predicate matched nothing at birth — usually too narrow. Ask the next reviewer to `SUPERSEDE` it |\n`BLOCKED — register debt from round N` |\nTwo attempts at a parseable register failed. The next round with a good register clears it |\n`unmechanized: awaiting reviewer CLOSED or RECLASSIFY` |\nA semantic class no regex can check |\n`STATE-UNAVAILABLE` |\nLineage state is unreadable, unwritable, or a previous write may not have completed. The message names the absolute path; repair or delete it, then re-run |\n\n`NOT-BLOCKED`\n\nasserts only that no blocking class is unclosed. It never asserts the\nchange is correct — the reviewer's findings still govern that.\n\n| Outcome | Meaning |\n|---|---|\n`CONVERGED` |\nUnanimous, unblocked, and each vote substantiated by a resolved citation |\n`BLOCKED` |\nThey agree on an option and one of them tags it `[MAJOR]` /`[FATAL]` |\n`REFRAME_REQUIRED` |\nA decider surfaced a better unlisted option. Give it an id and re-run |\n`UNRESOLVED` |\nStill split, or agreement nobody could substantiate |\n`FAILED` |\nPreflight, cleaning, parsing, or the repo's refs moved mid-run |\n\nThe reply ends with a machine-readable trailer whose fields are always present:\n`ARBITRATION`\n\n, `SELECTED`\n\n, `ADVISORY`\n\n, `AUTHORITY-POLICY`\n\n, `CLEANING`\n\n, `SNAPSHOT`\n\n,\n`ORDER-SEED`\n\n, `REFS-MOVED`\n\n, `AUDIT`\n\n, `ROUNDS`\n\n.\n\nDrop one at the repo root so callers stop retyping context. Keys go at the top\nlevel or under `[paranoia]`\n\n. Precedence: **call argument > .paranoia.toml >\nbuilt-in default**.\n\n```\nproject_summary = \"A booking API. Python/FastAPI, Postgres. Auth via short-lived JWTs.\"\nbase_ref = \"develop\"\nstakes = \"Internal booking API, single team, authenticated first-party callers, ~1k req/min.\"\nweb_search = true\nisolate = true\n```\n\nHonoured keys: `base_ref`\n\n, `project_summary`\n\n, `stakes`\n\n, `isolate`\n\n, `converge`\n\n,\n`class_closure`\n\n, `max_packet_chars`\n\n, `model`\n\n, `effort`\n\n, `web_search`\n\n.\n\n`critique_plan`\n\n's `class_closure`\n\nand `lineage`\n\nare **not** read from here.\n\n```\nparanoia-local --engine {codex|claude} [--log-dir DIR]\n```\n\n| Flag | Default | Description |\n|---|---|---|\n`--engine` |\nrequired |\nWhich local engine performs reviews — the other agent from the caller |\n`--log-dir` |\n`~/.paranoia/logs` |\nAudit-log directory |\n\n| Path | Contents |\n|---|---|\n`~/.paranoia/logs/` |\nOne JSON audit record per call: engine, model, round, `already_raised` , session ref, timings, and the review text |\n`~/.paranoia/lineages/` |\nClass-closure state, one file per lineage |\n\nLineage state deliberately does **not** follow `--log-dir`\n\n, so moving your logs\ncannot silently reset a tracked lineage. Set `PARANOIA_STATE_ROOT`\n\nto relocate it.\n\n-\n**Read-only.** Codex runs under its OS sandbox (`--sandbox read-only`\n\n); Claude runs with a read-only tool allowlist (`Read`\n\n,`Grep`\n\n,`Glob`\n\n, scoped`git`\n\nreads, web search) and write tools explicitly denied. The reviewer cannot edit your code, run your test suite, or reach the network except for opt-in web search. -\n**The audited repo cannot widen the reviewer.** The Claude engine is spawned with`--setting-sources \"\"`\n\n, so it loads no`.claude`\n\nsettings files — otherwise the reviewed repo's`.claude/settings.local.json`\n\nand your global settings would merge on top of the allowlist, and those routinely grant`Bash(python3:*)`\n\nand friends. This applies to the spawned reviewer subprocess only; it does not read, write, or affect your interactive`claude`\n\nsessions. Codex is covered by its OS-level sandbox, which no repo setting can loosen. -\n**Isolated.** Committed reviews run inside a throwaway`git worktree`\n\nof the target ref, so they never collide with your working tree and can review a branch that isn't checked out. Dirty-working-tree reviews necessarily run in the live repo, read-only. -\n**No API keys, no telemetry.** The server shells out to a CLI you are already signed into. -\n**Minimal footprint.** In`converge`\n\nmode the server creates a short-lived worktree and a few unreferenced git objects in the target repo. Both are cleaned up on exit and no ref is created. A hard crash can leave the worktree registration until the next`git worktree prune`\n\n/`git gc`\n\n. Your working tree and index are never touched.**One opt-in exception:**`arbitrate`\n\nwith`retain_snapshot: true`\n\ncreates`refs/paranoia/arbitrate/<stamp>`\n\nso its evidence survives`git gc`\n\n. It is the only mode in the server that writes a ref. Remove one with`git update-ref -d <ref>`\n\n.\n\nReviews draw on your subscription's agentic-usage pool, and a convergence loop is\nmany agent turns. Use `query`\n\nfor quick checks and reserve multi-round\n`critique_branch`\n\nloops for changes that warrant them.\n\n`arbitrate`\n\nis the expensive one and the only tool that spends from **both**\nsubscriptions in a single call: typically 4 agent turns, 8 at worst (a cleaning\nretry plus a reconciliation round).\n\n```\npip install -e '.[dev]'\npython -m pytest        # unit + integration; integration uses fake CLIs, no quota\n```\n\nThe engine subprocess boundary is dependency-injected, so the whole stack is\nunit-tested without spending subscription quota. A separate integration test drives\nthe real subprocess runner against fake `codex`\n\n/`claude`\n\nbinaries on `PATH`\n\n.\n\nDesign documents for the two non-obvious subsystems live in\n[ docs/](/subvertnormality/paranoia-local/blob/main/docs):\n\n[,](/subvertnormality/paranoia-local/blob/main/docs/class_closure_plan.md)\n\n`class_closure_plan.md`\n\n[, and](/subvertnormality/paranoia-local/blob/main/docs/plan_class_closure_proposal.md)\n\n`plan_class_closure_proposal.md`\n\n[.](/subvertnormality/paranoia-local/blob/main/docs/arbitration_plan.md)\n\n`arbitration_plan.md`\n\nMIT © 2026 Andrew Hillel", "url": "https://wpnews.pro/news/show-hn-paranoia-get-a-cold-adversarial-review-of-your-code", "canonical_source": "https://github.com/subvertnormality/paranoia-local", "published_at": "2026-08-04 10:07:03+00:00", "updated_at": "2026-08-04 10:22:50.634047+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "artificial-intelligence"], "entities": ["Paranoia", "subvertnormality", "Claude Code", "Codex", "MCP", "GPT-5"], "alternates": {"html": "https://wpnews.pro/news/show-hn-paranoia-get-a-cold-adversarial-review-of-your-code", "markdown": "https://wpnews.pro/news/show-hn-paranoia-get-a-cold-adversarial-review-of-your-code.md", "text": "https://wpnews.pro/news/show-hn-paranoia-get-a-cold-adversarial-review-of-your-code.txt", "jsonld": "https://wpnews.pro/news/show-hn-paranoia-get-a-cold-adversarial-review-of-your-code.jsonld"}}