Vibecode ZeroLeaks
track this build4 phases, 12 steps, beginner friendly0%The mechanics here are not exotic: fire a few hundred adversarial prompts at your own chat endpoint, capture the responses, and check whether any of them contain your system prompt, tool schemas or API keys. An agent can build that loop, including an LLM-as-judge scorer and an HTML report, in a single sitting, and it will find the embarrassing stuff on day one. What you cannot one-shot is a probe library that stays current with each new model release and each new jailbreak family, because that is maintained knowledge, not code. There is also a trust angle: 'we ran our own script and found nothing' reads very differently in a security review than a dated third-party report. Build it for your own sanity checks, keep paying if you need something to show someone else.
You are building a lean indie version of ZeroLeaks. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # ZeroLeaks · indie build A local extraction-probe harness whose hard problems are enforcing authorisation at the transport rather than in documentation, scoring a finding by what actually leaked rather than by whether a prompt came back, and judging transcripts without posting your own secrets to somebody else's model. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | A Python command line tool writing to a local run directory, no service and no dashboard | a security tool that phones home is a security problem | | Targeting | One target file naming exactly one host, enforced inside the HTTP transport | the difference between a test and an offence is whose machine it is | | Probes | A versioned directory of declarative probe files with a pack version and a date | the pack ages faster than the code and the report has to say how old it is | | Detection | Deterministic matchers first, a model judge second, deterministic always winning | a judge that overrules a regex match on your own key is worse than no judge | | Reporting | A self-contained HTML report plus JSON, with full transcripts and a diff between runs | the transcript is the finding; a severity label on its own proves nothing | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - Why: Runs the command line tool. - Get it: python.org or your package manager. - Verify: python3 --version prints 3.12 or higher - [ ] **Written authorisation to test the target** · free, and non-negotiable - Why: Authorisation is what separates this from an offence, and it is a required record in the tool rather than an assumption, so you need it before anything runs. - Get it: An email or signed note from whoever owns the endpoint, naming the host, the scope and the dates. - Verify: The authorisation record exists, names the host and has not expired - [ ] **A reachable chat endpoint for the application under test** · whatever your own endpoint costs to call - Why: The target file describes one endpoint, its request shape and where the reply text sits in the response, and getting that wrong means every probe scores as a refusal. - Get it: Point it at a staging deployment of your own application if you have one. - Verify: A single probe returns extracted reply text rather than a raw response body - [ ] **Your real system prompt text, tool names and secret patterns** · free - Why: The deterministic matchers are the reliable half of this build and they are worthless without the actual strings to match against. - Get it: Copy the current prompt and tool schema, and write patterns for every key format you use. - Verify: A crafted response containing each signal produces a finding - [ ] **An API key for a judge model** (optional) · pennies per run - Why: The judge catches paraphrased leakage the matchers miss, but it is optional by design because a run without it must still produce every deterministic finding. - Get it: Any provider whose model returns structured output reliably. - Verify: A redacted transcript returns strict JSON with a verdict ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Testing anything not named in the target file. - Denial of service, scanning or authentication bypass. - A hosted dashboard or shared findings database. - A numeric security score or badge. - A curated, maintained probe corpus that tracks new jailbreak families instead of whatever the agent remembered on build day - Multi-turn and multi-model attack strategies, including crescendo and encoding tricks, done properly rather than as single-shot prompts - A third-party report with a date on it that you can hand to a customer or an auditor - Severity triage and remediation guidance written by someone who has seen a lot of these - Regression runs on every model or prompt change without you remembering to trigger them If one of those is essential to you, that is the reason to keep paying for ZeroLeaks, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · ZeroLeaks The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local harness that fires a versioned pack of extraction probes at an endpoint you are authorised to test and reports exactly what came back to replace ZeroLeaks. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two rules. Neither is a preference. This tool only ever points at an endpoint you own or hold written permission to test, and that is a check in the code rather than a line in the README. The target lives in a local file naming one host. The transport refuses any other host on every request, including after a redirect. There is no discovery, no scanning, no target list, no flag that turns the check off, and no mode that accepts a host from the command line. Before the first run the tool requires an authorisation record naming who granted permission and when, and it writes that record into every report. Computer misuse law generally turns on authorisation rather than on whether any harm occurred, so get it in writing, check what applies where you and the target are, and keep the record. Second, be honest about what a finding is. The 2025 OWASP list for language model applications treats system prompt leakage as its own entry and is blunt: the system prompt should not be considered a secret, nor used as a security control, and credentials and connection strings should not be in it at all. So an extracted prompt is not the vulnerability, it is evidence about what was in the prompt. Severity comes from what leaked, not from which probe fired. And there is no score: a number derived from a pack you shipped describes the pack, not the endpoint. ### Stack (fixed, do not substitute) - Runtime: A Python command line tool writing to a local run directory, no service and no dashboard · a security tool that phones home is a security problem - Targeting: One target file naming exactly one host, enforced inside the HTTP transport · the difference between a test and an offence is whose machine it is - Probes: A versioned directory of declarative probe files with a pack version and a date · the pack ages faster than the code and the report has to say how old it is - Detection: Deterministic matchers first, a model judge second, deterministic always winning · a judge that overrules a regex match on your own key is worse than no judge - Reporting: A self-contained HTML report plus JSON, with full transcripts and a diff between runs · the transcript is the finding; a severity label on its own proves nothing ### Data model (create this before Phase 1) - authorisations(id, target_host, granted_by, granted_at, expires_at, evidence_path) · a run with no current row here does not start - targets(id, name, url, method, header_names_json, body_template, reply_path) · header values come from the environment and never from this file - signals(id, kind, value_ref, description) · kind is prompt_text, tool_name or secret_pattern, and the values live outside the repository - probes(id, pack_version, family, severity_hint, turns_json, added_at, source_note) - runs(id, target_id, pack_version, started_at, ended_at, authorisation_id, judge_model_id, probe_count, request_count) - attempts(id, run_id, probe_id, turn_ordinal, request_json, response_json, latency_ms, http_status) · verbatim, because this is the evidence - findings(id, run_id, probe_id, detector, matched_signal_id, severity, judge_json, first_seen_run_id) · severity is computed from which signal matched, never from which probe fired ### Phase 1 · A target you are allowed to test Build: Read the target file and the authorisation record. Enforce the host check inside the HTTP transport on every request, with automatic redirect following disabled and any cross-host redirect treated as a run-ending error. Print the authorisation and the scope before the first run. Trap: Checking the host once when the configuration loads leaves three ways out: a redirect, a proxy setting and a templated URL, any of which sends your adversarial traffic at somebody who never agreed to receive it and turns a test into an incident; put the check in the transport so it runs on every single request, turn off automatic redirect following, and make a cross-host attempt end the run rather than skip the request, so it is impossible to miss. Done when: A target whose URL redirects to another host ends the run with a named error and zero requests to that host, tested with a local redirect. Every request passes the host check, asserted by a test that patches the transport and counts checks against requests. A run without a current authorisation row refuses to start and says what is missing. There is no command line flag, environment variable or configuration key that disables the check, verified by review and by a test that greps for one. Do not build yet: No probes, detection, judging or reporting yet. ### Phase 2 · A probe pack that is data, not code Build: Define probes as declarative files with an identifier, a family, a severity hint, one or more turns and optional references to earlier replies. Version the pack as a whole with a date. Validate every file against a schema at load, and give the loader no way to execute anything. Trap: Encoding probes as Python functions means the pack cannot be updated without a release, cannot be reviewed by anyone who does not read your code, and eventually contains a probe that does something other than send a message, at which point your red-team harness has an arbitrary code execution path in it aimed at your own infrastructure; keep probes declarative, validate them at load, and make the loader structurally incapable of running anything. Done when: A new probe file is picked up with no code change, tested by adding one. A probe file that fails the schema is reported by name and the run refuses to start rather than skipping it. The pack version and date appear in every run row and every report. The loader contains no evaluation, import or subprocess call, verified by review and by a test that scans the loader module. Do not build yet: No detection, judging, concurrency or reporting yet. ### Phase 3 · Detection in the right order Build: Run deterministic matchers first: token overlap against your known prompt text at a stated threshold, exact tool name matches, and secret patterns over the raw response. Then send the judge a transcript with every matched span replaced by a marker, and record what was redacted. Trap: Sending the full transcript to a third-party judge model means the tool built to find out whether your system prompt and your keys leak has just posted your system prompt and your keys into somebody else's inference logs, which is the exact outcome you were testing for; match deterministically on your own machine, replace every matched span with a marker before the judge sees anything, record which markers were substituted, and make the judge optional so a run with no judge key still produces every deterministic finding. Done when: A response containing a known secret pattern produces a finding without the secret ever appearing in a judge request, asserted by a test that inspects the outbound judge payload. Deterministic results override a contradicting judge verdict, tested with a judge stubbed to disagree. Severity is derived from the matched signal kind and not from the probe's severity hint, asserted by a test where a low-hint probe extracts a key. With no judge configured the run completes and reports every deterministic finding. Do not build yet: No multi-turn sequencing, rate control, reporting or diffing yet. ### Phase 4 · Multi-turn, and not knocking the target over Build: Support turns that reference earlier replies, with bounded concurrency defaulting low, a per-request timeout, backoff that honours the retry interval the server sends, a hard cap on requests per run and a limit flag for smoke tests. Stop the run when the error rate crosses a threshold. Trap: A harness with cheerful default concurrency pointed at your own production endpoint is an unplanned load test, and one that retries a rate-limit response immediately converts a rate limit into an outage you caused yourself on the system you were trying to protect; default the concurrency low, honour the interval the server asks for, cap total requests per run, and abort with a summary when the error rate crosses a threshold rather than grinding on and blaming the target. Done when: A stubbed rate-limit response with a retry interval causes a wait of at least that interval, tested. Total requests in a run never exceed the configured cap, asserted by a counting transport. An error rate above the threshold aborts the run with a partial report rather than continuing, tested. A multi-turn probe whose later turn references an earlier reply receives the actual reply text, tested end to end against a local stub. Do not build yet: No report rendering, diffing or exit codes yet. ### Phase 5 · A report that does not overclaim Build: Render a self-contained HTML report grouped by what leaked, with the pack version, pack date, probe count, families covered and the authorisation record at the top. Emit JSON for diffing. Add a diff command showing newly failing and newly passing probes, and exit non-zero on a high finding. Trap: A summary line reading 'no leaks found' will be pasted into a security questionnaire within a month, and it is not true: what happened is that this pack, on this date, against this endpoint, found nothing, while the jailbreak families it knows about are a snapshot of what its author understood when they wrote it; put the pack date, probe count and covered families in the summary, phrase the result as what was tested rather than what is safe, and state in the report itself that a self-run harness is not a third-party assessment. Done when: Every report carries the pack version, pack date, probe count, covered families and the authorisation record above the findings, asserted in the rendering test. The summary states what was tested and never asserts the endpoint is safe, checked by a test that fails on an assurance phrase. The diff command lists newly failing and newly passing probes between two runs and is stable when nothing changed. A high severity finding exits non-zero and a clean run exits zero, tested both ways. Do not build yet: Nothing after this. ### Out of scope (and why) - Testing, scanning or discovering anything not named in the target file, and there is deliberately no flag, variable or mode that permits it. - Denial of service, rate limit abuse, network scanning and authentication bypass testing, none of which this tool does and all of which need a different conversation about permission. - A hosted dashboard or a shared findings database, which would mean holding other people's leaked prompts and secrets on your infrastructure. - A numeric security score or a badge, because a number derived from the pack you happen to ship describes the pack rather than the endpoint. ### README must contain - That the tool refuses any host except the one in the target file, enforced per request including redirects, and that there is no way to turn that off - That an authorisation record is required before the first run and is printed into every report - That a leaked system prompt is evidence rather than the vulnerability, and that severity comes from what leaked - That the judge only ever sees a transcript with matched spans replaced by markers, and that the judge is optional - That a clean run means this pack on this date found nothing, and that it is not a third-party assessment ===== AGENTS.md ===== # Agent instructions · ZeroLeaks indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A Python command line tool writing to a local run directory, no service and no dashboard, One target file naming exactly one host, enforced inside the HTTP transport, A versioned directory of declarative probe files with a pack version and a date, Deterministic matchers first, a model judge second, deterministic always winning, A self-contained HTML report plus JSON, with full transcripts and a diff between runs. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Checking the host once when the configuration loads leaves three ways out: a redirect, a proxy setting and a templated URL, any of which sends your adversarial traffic at somebody who never agreed to receive it and turns a test into an incident; put the check in the transport so it runs on every single request, turn off automatic redirect following, and make a cross-host attempt end the run rather than skip the request, so it is impossible to miss. - Encoding probes as Python functions means the pack cannot be updated without a release, cannot be reviewed by anyone who does not read your code, and eventually contains a probe that does something other than send a message, at which point your red-team harness has an arbitrary code execution path in it aimed at your own infrastructure; keep probes declarative, validate them at load, and make the loader structurally incapable of running anything. - Sending the full transcript to a third-party judge model means the tool built to find out whether your system prompt and your keys leak has just posted your system prompt and your keys into somebody else's inference logs, which is the exact outcome you were testing for; match deterministically on your own machine, replace every matched span with a marker before the judge sees anything, record which markers were substituted, and make the judge optional so a run with no judge key still produces every deterministic finding. - A harness with cheerful default concurrency pointed at your own production endpoint is an unplanned load test, and one that retries a rate-limit response immediately converts a rate limit into an outage you caused yourself on the system you were trying to protect; default the concurrency low, honour the interval the server asks for, cap total requests per run, and abort with a summary when the error rate crosses a threshold rather than grinding on and blaming the target. ===== BUILD_PLAN.md ===== # Build plan · ZeroLeaks A local extraction-probe harness whose hard problems are enforcing authorisation at the transport rather than in documentation, scoring a finding by what actually leaked rather than by whether a prompt came back, and judging transcripts without posting your own secrets to somebody else's model. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A target you are allowed to test The tool cannot send a request to any host other than the one you recorded permission for. ### Steps 1. Enforce the host inside the transport, per request Configuration-time checks have three documented ways around them. Files: `leakprobe/transport.py`, `tests/test_host_guard.py` 2. Disable redirect following and fail the run on a cross-host hop A redirect is the easiest way to point this at a stranger. Files: `leakprobe/transport.py` 3. Require an authorisation record and print it It goes in the report too, because that is where it matters. Files: `leakprobe/auth.py`, `tests/test_auth_required.py` ### Done when - [ ] A cross-host redirect ends the run with zero requests - [ ] Every request passes the host check - [ ] No authorisation means no run - [ ] No flag or variable disables the check ### Watch out - Checking the host once when the configuration loads leaves three ways out: a redirect, a proxy setting and a templated URL, any of which sends your adversarial traffic at somebody who never agreed to receive it and turns a test into an incident; put the check in the transport so it runs on every single request, turn off automatic redirect following, and make a cross-host attempt end the run rather than skip the request, so it is impossible to miss. ## Phase 2 · A probe pack that is data, not code Probes can be added, reviewed and dated without touching the program. ### Steps 1. Define the probe schema and validate at load A bad probe stops the run; it does not get skipped. Files: `leakprobe/probes/schema.py`, `tests/test_probe_schema.py` 2. Stamp the pack version and date into every run A report without a pack date is a report with no shelf life. Files: `leakprobe/pack.py` 3. Keep the loader incapable of executing anything No eval, no import, no subprocess, and a test that says so. Files: `tests/test_loader_inert.py` ### Done when - [ ] A new probe file works with no code change - [ ] An invalid probe stops the run by name - [ ] Pack version and date reach every report - [ ] The loader executes nothing ### Watch out - Encoding probes as Python functions means the pack cannot be updated without a release, cannot be reviewed by anyone who does not read your code, and eventually contains a probe that does something other than send a message, at which point your red-team harness has an arbitrary code execution path in it aimed at your own infrastructure; keep probes declarative, validate them at load, and make the loader structurally incapable of running anything. ## Phase 3 · Detection in the right order A deterministic match is the finding, and the judge never sees your secrets. ### Steps 1. Match deterministically before anything leaves the machine The matchers are cheap and they are the reliable half. Files: `leakprobe/detect.py`, `tests/test_detectors.py` 2. Redact matched spans before the judge sees the transcript Then record which markers stood in for what. Files: `leakprobe/judge.py`, `tests/test_judge_redaction.py` 3. Compute severity from the signal, not the probe A prompt with nothing in it is not a critical finding. Files: `leakprobe/severity.py` ### Done when - [ ] No secret reaches the judge request - [ ] Deterministic results beat the judge - [ ] Severity follows the matched signal - [ ] A run without a judge still reports findings ### Watch out - Sending the full transcript to a third-party judge model means the tool built to find out whether your system prompt and your keys leak has just posted your system prompt and your keys into somebody else's inference logs, which is the exact outcome you were testing for; match deterministically on your own machine, replace every matched span with a marker before the judge sees anything, record which markers were substituted, and make the judge optional so a run with no judge key still produces every deterministic finding. ## Phase 4 · Multi-turn, and not knocking the target over A run explores multi-turn attacks while staying gentle enough to point at something real. ### Steps 1. Default concurrency low and cap requests per run This is pointed at something somebody depends on. Files: `leakprobe/runner.py`, `tests/test_cap.py` 2. Honour the retry interval the server sends Ignoring it is how a rate limit becomes an outage. Files: `leakprobe/backoff.py` 3. Abort on a high error rate with a partial report A thousand failed requests is not more data. Files: `leakprobe/runner.py` ### Done when - [ ] A retry interval is honoured in full - [ ] The request cap is never exceeded - [ ] A high error rate aborts with a partial report - [ ] Multi-turn probes see the real earlier reply ### Watch out - A harness with cheerful default concurrency pointed at your own production endpoint is an unplanned load test, and one that retries a rate-limit response immediately converts a rate limit into an outage you caused yourself on the system you were trying to protect; default the concurrency low, honour the interval the server asks for, cap total requests per run, and abort with a summary when the error rate crosses a threshold rather than grinding on and blaming the target. ## Not in this build - Testing anything not named in the target file. - Denial of service, scanning or authentication bypass. - A hosted dashboard or shared findings database. - A numeric security score or badge. ## After v1, if you want it - A scheduled run against staging on every prompt change, which is the only use of this that catches anything before a customer does - A second signals file for a different deployment, which tests whether severity really was computed from signals - A written note beside each probe family saying where it came from and when, so the pack ages visibly rather than silently ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The one endpoint. Its host is the only host the transport will speak to. TARGET_FILE=./target.yaml # Required. Who granted permission, when, and until when. Printed into every report. AUTHORISATION_FILE=./authorisation.yaml # Required. Your prompt text, tool names and secret patterns. Never committed. SIGNALS_FILE=./signals.yaml # Required · secret. The credential for your own endpoint. Referenced by name from the target file. TARGET_AUTH_HEADER=Bearer ...redacted # Optional · secret. Optional. Without it the run reports every deterministic finding and no judge verdicts. JUDGE_API_KEY=sk-...redacted # Required. A hard cap. The run stops here whatever the pack says. MAX_REQUESTS_PER_RUN=600 # Required. Low by default. This is pointed at something somebody depends on. CONCURRENCY=2 # Required. Transcripts and reports. These contain whatever leaked, so treat them as secret. RUNS_DIR=./runs
You are building a lean indie version of ZeroLeaks. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # ZeroLeaks · indie build A local extraction-probe harness whose hard problems are enforcing authorisation at the transport rather than in documentation, scoring a finding by what actually leaked rather than by whether a prompt came back, and judging transcripts without posting your own secrets to somebody else's model. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | A Python command line tool writing to a local run directory, no service and no dashboard | a security tool that phones home is a security problem | | Targeting | One target file naming exactly one host, enforced inside the HTTP transport | the difference between a test and an offence is whose machine it is | | Probes | A versioned directory of declarative probe files with a pack version and a date | the pack ages faster than the code and the report has to say how old it is | | Detection | Deterministic matchers first, a model judge second, deterministic always winning | a judge that overrules a regex match on your own key is worse than no judge | | Reporting | A self-contained HTML report plus JSON, with full transcripts and a diff between runs | the transcript is the finding; a severity label on its own proves nothing | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - Why: Runs the command line tool. - Get it: python.org or your package manager. - Verify: python3 --version prints 3.12 or higher - [ ] **Written authorisation to test the target** · free, and non-negotiable - Why: Authorisation is what separates this from an offence, and it is a required record in the tool rather than an assumption, so you need it before anything runs. - Get it: An email or signed note from whoever owns the endpoint, naming the host, the scope and the dates. - Verify: The authorisation record exists, names the host and has not expired - [ ] **A reachable chat endpoint for the application under test** · whatever your own endpoint costs to call - Why: The target file describes one endpoint, its request shape and where the reply text sits in the response, and getting that wrong means every probe scores as a refusal. - Get it: Point it at a staging deployment of your own application if you have one. - Verify: A single probe returns extracted reply text rather than a raw response body - [ ] **Your real system prompt text, tool names and secret patterns** · free - Why: The deterministic matchers are the reliable half of this build and they are worthless without the actual strings to match against. - Get it: Copy the current prompt and tool schema, and write patterns for every key format you use. - Verify: A crafted response containing each signal produces a finding - [ ] **An API key for a judge model** (optional) · pennies per run - Why: The judge catches paraphrased leakage the matchers miss, but it is optional by design because a run without it must still produce every deterministic finding. - Get it: Any provider whose model returns structured output reliably. - Verify: A redacted transcript returns strict JSON with a verdict ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Testing anything not named in the target file. - Denial of service, scanning or authentication bypass. - A hosted dashboard or shared findings database. - A numeric security score or badge. - A curated, maintained probe corpus that tracks new jailbreak families instead of whatever the agent remembered on build day - Multi-turn and multi-model attack strategies, including crescendo and encoding tricks, done properly rather than as single-shot prompts - A third-party report with a date on it that you can hand to a customer or an auditor - Severity triage and remediation guidance written by someone who has seen a lot of these - Regression runs on every model or prompt change without you remembering to trigger them If one of those is essential to you, that is the reason to keep paying for ZeroLeaks, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · ZeroLeaks The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local harness that fires a versioned pack of extraction probes at an endpoint you are authorised to test and reports exactly what came back to replace ZeroLeaks. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two rules. Neither is a preference. This tool only ever points at an endpoint you own or hold written permission to test, and that is a check in the code rather than a line in the README. The target lives in a local file naming one host. The transport refuses any other host on every request, including after a redirect. There is no discovery, no scanning, no target list, no flag that turns the check off, and no mode that accepts a host from the command line. Before the first run the tool requires an authorisation record naming who granted permission and when, and it writes that record into every report. Computer misuse law generally turns on authorisation rather than on whether any harm occurred, so get it in writing, check what applies where you and the target are, and keep the record. Second, be honest about what a finding is. The 2025 OWASP list for language model applications treats system prompt leakage as its own entry and is blunt: the system prompt should not be considered a secret, nor used as a security control, and credentials and connection strings should not be in it at all. So an extracted prompt is not the vulnerability, it is evidence about what was in the prompt. Severity comes from what leaked, not from which probe fired. And there is no score: a number derived from a pack you shipped describes the pack, not the endpoint. ### Stack (fixed, do not substitute) - Runtime: A Python command line tool writing to a local run directory, no service and no dashboard · a security tool that phones home is a security problem - Targeting: One target file naming exactly one host, enforced inside the HTTP transport · the difference between a test and an offence is whose machine it is - Probes: A versioned directory of declarative probe files with a pack version and a date · the pack ages faster than the code and the report has to say how old it is - Detection: Deterministic matchers first, a model judge second, deterministic always winning · a judge that overrules a regex match on your own key is worse than no judge - Reporting: A self-contained HTML report plus JSON, with full transcripts and a diff between runs · the transcript is the finding; a severity label on its own proves nothing ### Data model (create this before Phase 1) - authorisations(id, target_host, granted_by, granted_at, expires_at, evidence_path) · a run with no current row here does not start - targets(id, name, url, method, header_names_json, body_template, reply_path) · header values come from the environment and never from this file - signals(id, kind, value_ref, description) · kind is prompt_text, tool_name or secret_pattern, and the values live outside the repository - probes(id, pack_version, family, severity_hint, turns_json, added_at, source_note) - runs(id, target_id, pack_version, started_at, ended_at, authorisation_id, judge_model_id, probe_count, request_count) - attempts(id, run_id, probe_id, turn_ordinal, request_json, response_json, latency_ms, http_status) · verbatim, because this is the evidence - findings(id, run_id, probe_id, detector, matched_signal_id, severity, judge_json, first_seen_run_id) · severity is computed from which signal matched, never from which probe fired ### Phase 1 · A target you are allowed to test Build: Read the target file and the authorisation record. Enforce the host check inside the HTTP transport on every request, with automatic redirect following disabled and any cross-host redirect treated as a run-ending error. Print the authorisation and the scope before the first run. Trap: Checking the host once when the configuration loads leaves three ways out: a redirect, a proxy setting and a templated URL, any of which sends your adversarial traffic at somebody who never agreed to receive it and turns a test into an incident; put the check in the transport so it runs on every single request, turn off automatic redirect following, and make a cross-host attempt end the run rather than skip the request, so it is impossible to miss. Done when: A target whose URL redirects to another host ends the run with a named error and zero requests to that host, tested with a local redirect. Every request passes the host check, asserted by a test that patches the transport and counts checks against requests. A run without a current authorisation row refuses to start and says what is missing. There is no command line flag, environment variable or configuration key that disables the check, verified by review and by a test that greps for one. Do not build yet: No probes, detection, judging or reporting yet. ### Phase 2 · A probe pack that is data, not code Build: Define probes as declarative files with an identifier, a family, a severity hint, one or more turns and optional references to earlier replies. Version the pack as a whole with a date. Validate every file against a schema at load, and give the loader no way to execute anything. Trap: Encoding probes as Python functions means the pack cannot be updated without a release, cannot be reviewed by anyone who does not read your code, and eventually contains a probe that does something other than send a message, at which point your red-team harness has an arbitrary code execution path in it aimed at your own infrastructure; keep probes declarative, validate them at load, and make the loader structurally incapable of running anything. Done when: A new probe file is picked up with no code change, tested by adding one. A probe file that fails the schema is reported by name and the run refuses to start rather than skipping it. The pack version and date appear in every run row and every report. The loader contains no evaluation, import or subprocess call, verified by review and by a test that scans the loader module. Do not build yet: No detection, judging, concurrency or reporting yet. ### Phase 3 · Detection in the right order Build: Run deterministic matchers first: token overlap against your known prompt text at a stated threshold, exact tool name matches, and secret patterns over the raw response. Then send the judge a transcript with every matched span replaced by a marker, and record what was redacted. Trap: Sending the full transcript to a third-party judge model means the tool built to find out whether your system prompt and your keys leak has just posted your system prompt and your keys into somebody else's inference logs, which is the exact outcome you were testing for; match deterministically on your own machine, replace every matched span with a marker before the judge sees anything, record which markers were substituted, and make the judge optional so a run with no judge key still produces every deterministic finding. Done when: A response containing a known secret pattern produces a finding without the secret ever appearing in a judge request, asserted by a test that inspects the outbound judge payload. Deterministic results override a contradicting judge verdict, tested with a judge stubbed to disagree. Severity is derived from the matched signal kind and not from the probe's severity hint, asserted by a test where a low-hint probe extracts a key. With no judge configured the run completes and reports every deterministic finding. Do not build yet: No multi-turn sequencing, rate control, reporting or diffing yet. ### Phase 4 · Multi-turn, and not knocking the target over Build: Support turns that reference earlier replies, with bounded concurrency defaulting low, a per-request timeout, backoff that honours the retry interval the server sends, a hard cap on requests per run and a limit flag for smoke tests. Stop the run when the error rate crosses a threshold. Trap: A harness with cheerful default concurrency pointed at your own production endpoint is an unplanned load test, and one that retries a rate-limit response immediately converts a rate limit into an outage you caused yourself on the system you were trying to protect; default the concurrency low, honour the interval the server asks for, cap total requests per run, and abort with a summary when the error rate crosses a threshold rather than grinding on and blaming the target. Done when: A stubbed rate-limit response with a retry interval causes a wait of at least that interval, tested. Total requests in a run never exceed the configured cap, asserted by a counting transport. An error rate above the threshold aborts the run with a partial report rather than continuing, tested. A multi-turn probe whose later turn references an earlier reply receives the actual reply text, tested end to end against a local stub. Do not build yet: No report rendering, diffing or exit codes yet. ### Phase 5 · A report that does not overclaim Build: Render a self-contained HTML report grouped by what leaked, with the pack version, pack date, probe count, families covered and the authorisation record at the top. Emit JSON for diffing. Add a diff command showing newly failing and newly passing probes, and exit non-zero on a high finding. Trap: A summary line reading 'no leaks found' will be pasted into a security questionnaire within a month, and it is not true: what happened is that this pack, on this date, against this endpoint, found nothing, while the jailbreak families it knows about are a snapshot of what its author understood when they wrote it; put the pack date, probe count and covered families in the summary, phrase the result as what was tested rather than what is safe, and state in the report itself that a self-run harness is not a third-party assessment. Done when: Every report carries the pack version, pack date, probe count, covered families and the authorisation record above the findings, asserted in the rendering test. The summary states what was tested and never asserts the endpoint is safe, checked by a test that fails on an assurance phrase. The diff command lists newly failing and newly passing probes between two runs and is stable when nothing changed. A high severity finding exits non-zero and a clean run exits zero, tested both ways. Do not build yet: Nothing after this. ### Out of scope (and why) - Testing, scanning or discovering anything not named in the target file, and there is deliberately no flag, variable or mode that permits it. - Denial of service, rate limit abuse, network scanning and authentication bypass testing, none of which this tool does and all of which need a different conversation about permission. - A hosted dashboard or a shared findings database, which would mean holding other people's leaked prompts and secrets on your infrastructure. - A numeric security score or a badge, because a number derived from the pack you happen to ship describes the pack rather than the endpoint. ### README must contain - That the tool refuses any host except the one in the target file, enforced per request including redirects, and that there is no way to turn that off - That an authorisation record is required before the first run and is printed into every report - That a leaked system prompt is evidence rather than the vulnerability, and that severity comes from what leaked - That the judge only ever sees a transcript with matched spans replaced by markers, and that the judge is optional - That a clean run means this pack on this date found nothing, and that it is not a third-party assessment ===== AGENTS.md ===== # Agent instructions · ZeroLeaks indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A Python command line tool writing to a local run directory, no service and no dashboard, One target file naming exactly one host, enforced inside the HTTP transport, A versioned directory of declarative probe files with a pack version and a date, Deterministic matchers first, a model judge second, deterministic always winning, A self-contained HTML report plus JSON, with full transcripts and a diff between runs. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Checking the host once when the configuration loads leaves three ways out: a redirect, a proxy setting and a templated URL, any of which sends your adversarial traffic at somebody who never agreed to receive it and turns a test into an incident; put the check in the transport so it runs on every single request, turn off automatic redirect following, and make a cross-host attempt end the run rather than skip the request, so it is impossible to miss. - Encoding probes as Python functions means the pack cannot be updated without a release, cannot be reviewed by anyone who does not read your code, and eventually contains a probe that does something other than send a message, at which point your red-team harness has an arbitrary code execution path in it aimed at your own infrastructure; keep probes declarative, validate them at load, and make the loader structurally incapable of running anything. - Sending the full transcript to a third-party judge model means the tool built to find out whether your system prompt and your keys leak has just posted your system prompt and your keys into somebody else's inference logs, which is the exact outcome you were testing for; match deterministically on your own machine, replace every matched span with a marker before the judge sees anything, record which markers were substituted, and make the judge optional so a run with no judge key still produces every deterministic finding. - A harness with cheerful default concurrency pointed at your own production endpoint is an unplanned load test, and one that retries a rate-limit response immediately converts a rate limit into an outage you caused yourself on the system you were trying to protect; default the concurrency low, honour the interval the server asks for, cap total requests per run, and abort with a summary when the error rate crosses a threshold rather than grinding on and blaming the target. ===== BUILD_PLAN.md ===== # Build plan · ZeroLeaks A local extraction-probe harness whose hard problems are enforcing authorisation at the transport rather than in documentation, scoring a finding by what actually leaked rather than by whether a prompt came back, and judging transcripts without posting your own secrets to somebody else's model. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A target you are allowed to test The tool cannot send a request to any host other than the one you recorded permission for. ### Steps 1. Enforce the host inside the transport, per request Configuration-time checks have three documented ways around them. Files: `leakprobe/transport.py`, `tests/test_host_guard.py` 2. Disable redirect following and fail the run on a cross-host hop A redirect is the easiest way to point this at a stranger. Files: `leakprobe/transport.py` 3. Require an authorisation record and print it It goes in the report too, because that is where it matters. Files: `leakprobe/auth.py`, `tests/test_auth_required.py` ### Done when - [ ] A cross-host redirect ends the run with zero requests - [ ] Every request passes the host check - [ ] No authorisation means no run - [ ] No flag or variable disables the check ### Watch out - Checking the host once when the configuration loads leaves three ways out: a redirect, a proxy setting and a templated URL, any of which sends your adversarial traffic at somebody who never agreed to receive it and turns a test into an incident; put the check in the transport so it runs on every single request, turn off automatic redirect following, and make a cross-host attempt end the run rather than skip the request, so it is impossible to miss. ## Phase 2 · A probe pack that is data, not code Probes can be added, reviewed and dated without touching the program. ### Steps 1. Define the probe schema and validate at load A bad probe stops the run; it does not get skipped. Files: `leakprobe/probes/schema.py`, `tests/test_probe_schema.py` 2. Stamp the pack version and date into every run A report without a pack date is a report with no shelf life. Files: `leakprobe/pack.py` 3. Keep the loader incapable of executing anything No eval, no import, no subprocess, and a test that says so. Files: `tests/test_loader_inert.py` ### Done when - [ ] A new probe file works with no code change - [ ] An invalid probe stops the run by name - [ ] Pack version and date reach every report - [ ] The loader executes nothing ### Watch out - Encoding probes as Python functions means the pack cannot be updated without a release, cannot be reviewed by anyone who does not read your code, and eventually contains a probe that does something other than send a message, at which point your red-team harness has an arbitrary code execution path in it aimed at your own infrastructure; keep probes declarative, validate them at load, and make the loader structurally incapable of running anything. ## Phase 3 · Detection in the right order A deterministic match is the finding, and the judge never sees your secrets. ### Steps 1. Match deterministically before anything leaves the machine The matchers are cheap and they are the reliable half. Files: `leakprobe/detect.py`, `tests/test_detectors.py` 2. Redact matched spans before the judge sees the transcript Then record which markers stood in for what. Files: `leakprobe/judge.py`, `tests/test_judge_redaction.py` 3. Compute severity from the signal, not the probe A prompt with nothing in it is not a critical finding. Files: `leakprobe/severity.py` ### Done when - [ ] No secret reaches the judge request - [ ] Deterministic results beat the judge - [ ] Severity follows the matched signal - [ ] A run without a judge still reports findings ### Watch out - Sending the full transcript to a third-party judge model means the tool built to find out whether your system prompt and your keys leak has just posted your system prompt and your keys into somebody else's inference logs, which is the exact outcome you were testing for; match deterministically on your own machine, replace every matched span with a marker before the judge sees anything, record which markers were substituted, and make the judge optional so a run with no judge key still produces every deterministic finding. ## Phase 4 · Multi-turn, and not knocking the target over A run explores multi-turn attacks while staying gentle enough to point at something real. ### Steps 1. Default concurrency low and cap requests per run This is pointed at something somebody depends on. Files: `leakprobe/runner.py`, `tests/test_cap.py` 2. Honour the retry interval the server sends Ignoring it is how a rate limit becomes an outage. Files: `leakprobe/backoff.py` 3. Abort on a high error rate with a partial report A thousand failed requests is not more data. Files: `leakprobe/runner.py` ### Done when - [ ] A retry interval is honoured in full - [ ] The request cap is never exceeded - [ ] A high error rate aborts with a partial report - [ ] Multi-turn probes see the real earlier reply ### Watch out - A harness with cheerful default concurrency pointed at your own production endpoint is an unplanned load test, and one that retries a rate-limit response immediately converts a rate limit into an outage you caused yourself on the system you were trying to protect; default the concurrency low, honour the interval the server asks for, cap total requests per run, and abort with a summary when the error rate crosses a threshold rather than grinding on and blaming the target. ## Not in this build - Testing anything not named in the target file. - Denial of service, scanning or authentication bypass. - A hosted dashboard or shared findings database. - A numeric security score or badge. ## After v1, if you want it - A scheduled run against staging on every prompt change, which is the only use of this that catches anything before a customer does - A second signals file for a different deployment, which tests whether severity really was computed from signals - A written note beside each probe family saying where it came from and when, so the pack ages visibly rather than silently ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The one endpoint. Its host is the only host the transport will speak to. TARGET_FILE=./target.yaml # Required. Who granted permission, when, and until when. Printed into every report. AUTHORISATION_FILE=./authorisation.yaml # Required. Your prompt text, tool names and secret patterns. Never committed. SIGNALS_FILE=./signals.yaml # Required · secret. The credential for your own endpoint. Referenced by name from the target file. TARGET_AUTH_HEADER=Bearer ...redacted # Optional · secret. Optional. Without it the run reports every deterministic finding and no judge verdicts. JUDGE_API_KEY=sk-...redacted # Required. A hard cap. The run stops here whatever the pack says. MAX_REQUESTS_PER_RUN=600 # Required. Low by default. This is pointed at something somebody depends on. CONCURRENCY=2 # Required. Transcripts and reports. These contain whatever leaked, so treat them as secret. RUNS_DIR=./runs
You are building a production product version of ZeroLeaks. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # ZeroLeaks · product brief ## Problem The mechanics here are not exotic: fire a few hundred adversarial prompts at your own chat endpoint, capture the responses, and check whether any of them contain your system prompt, tool schemas or API keys. An agent can build that loop, including an LLM-as-judge scorer and an HTML report, in a single sitting, and it will find the embarrassing stuff on day one. What you cannot one-shot is a probe library that stays current with each new model release and each new jailbreak family, because that is maintained knowledge, not code. There is also a trust angle: 'we ran our own script and found nothing' reads very differently in a security review than a dated third-party report. Build it for your own sanity checks, keep paying if you need something to show someone else. ## Product outcome A harness that cannot be pointed at somebody else's endpoint, scores a finding by what actually leaked rather than by whether a prompt came back, never shows your secrets to the judge, and produces a report nobody can honestly mistake for an assurance. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - An API key for the model you use as judge - A reachable chat endpoint or API for the app under test, plus permission to hammer it - A copy of your real system prompt and secret patterns to match against ## Explicit non-goals for v1 - Testing anything not named in the target file. - Denial of service, scanning or authentication bypass. - A hosted dashboard or shared findings database. - A numeric security score or badge. - A curated, maintained probe corpus that tracks new jailbreak families instead of whatever the agent remembered on build day - Multi-turn and multi-model attack strategies, including crescendo and encoding tricks, done properly rather than as single-shot prompts - A third-party report with a date on it that you can hand to a customer or an auditor - Severity triage and remediation guidance written by someone who has seen a lot of these - Regression runs on every model or prompt change without you remembering to trigger them ## Success criteria - A cross-host redirect is confirmed to end the run with zero requests - No flag or variable is confirmed to disable the host check - No secret is confirmed to reach the judge request - Severity is confirmed to follow the matched signal, not the probe - No assurance phrasing is confirmed to survive the report test ===== BRIEF.md ===== # Build brief · ZeroLeaks The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local harness that fires a versioned pack of extraction probes at an endpoint you are authorised to test and reports exactly what came back to replace ZeroLeaks. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two rules. Neither is a preference. This tool only ever points at an endpoint you own or hold written permission to test, and that is a check in the code rather than a line in the README. The target lives in a local file naming one host. The transport refuses any other host on every request, including after a redirect. There is no discovery, no scanning, no target list, no flag that turns the check off, and no mode that accepts a host from the command line. Before the first run the tool requires an authorisation record naming who granted permission and when, and it writes that record into every report. Computer misuse law generally turns on authorisation rather than on whether any harm occurred, so get it in writing, check what applies where you and the target are, and keep the record. Second, be honest about what a finding is. The 2025 OWASP list for language model applications treats system prompt leakage as its own entry and is blunt: the system prompt should not be considered a secret, nor used as a security control, and credentials and connection strings should not be in it at all. So an extracted prompt is not the vulnerability, it is evidence about what was in the prompt. Severity comes from what leaked, not from which probe fired. And there is no score: a number derived from a pack you shipped describes the pack, not the endpoint. ### Stack (fixed, do not substitute) - Runtime: A Python command line tool writing to a local run directory, no service and no dashboard · a security tool that phones home is a security problem - Targeting: One target file naming exactly one host, enforced inside the HTTP transport · the difference between a test and an offence is whose machine it is - Probes: A versioned directory of declarative probe files with a pack version and a date · the pack ages faster than the code and the report has to say how old it is - Detection: Deterministic matchers first, a model judge second, deterministic always winning · a judge that overrules a regex match on your own key is worse than no judge - Reporting: A self-contained HTML report plus JSON, with full transcripts and a diff between runs · the transcript is the finding; a severity label on its own proves nothing ### Data model (create this before Phase 1) - authorisations(id, target_host, granted_by, granted_at, expires_at, evidence_path) · a run with no current row here does not start - targets(id, name, url, method, header_names_json, body_template, reply_path) · header values come from the environment and never from this file - signals(id, kind, value_ref, description) · kind is prompt_text, tool_name or secret_pattern, and the values live outside the repository - probes(id, pack_version, family, severity_hint, turns_json, added_at, source_note) - runs(id, target_id, pack_version, started_at, ended_at, authorisation_id, judge_model_id, probe_count, request_count) - attempts(id, run_id, probe_id, turn_ordinal, request_json, response_json, latency_ms, http_status) · verbatim, because this is the evidence - findings(id, run_id, probe_id, detector, matched_signal_id, severity, judge_json, first_seen_run_id) · severity is computed from which signal matched, never from which probe fired ### Phase 1 · A target you are allowed to test Build: Read the target file and the authorisation record. Enforce the host check inside the HTTP transport on every request, with automatic redirect following disabled and any cross-host redirect treated as a run-ending error. Print the authorisation and the scope before the first run. Trap: Checking the host once when the configuration loads leaves three ways out: a redirect, a proxy setting and a templated URL, any of which sends your adversarial traffic at somebody who never agreed to receive it and turns a test into an incident; put the check in the transport so it runs on every single request, turn off automatic redirect following, and make a cross-host attempt end the run rather than skip the request, so it is impossible to miss. Done when: A target whose URL redirects to another host ends the run with a named error and zero requests to that host, tested with a local redirect. Every request passes the host check, asserted by a test that patches the transport and counts checks against requests. A run without a current authorisation row refuses to start and says what is missing. There is no command line flag, environment variable or configuration key that disables the check, verified by review and by a test that greps for one. Do not build yet: No probes, detection, judging or reporting yet. ### Phase 2 · A probe pack that is data, not code Build: Define probes as declarative files with an identifier, a family, a severity hint, one or more turns and optional references to earlier replies. Version the pack as a whole with a date. Validate every file against a schema at load, and give the loader no way to execute anything. Trap: Encoding probes as Python functions means the pack cannot be updated without a release, cannot be reviewed by anyone who does not read your code, and eventually contains a probe that does something other than send a message, at which point your red-team harness has an arbitrary code execution path in it aimed at your own infrastructure; keep probes declarative, validate them at load, and make the loader structurally incapable of running anything. Done when: A new probe file is picked up with no code change, tested by adding one. A probe file that fails the schema is reported by name and the run refuses to start rather than skipping it. The pack version and date appear in every run row and every report. The loader contains no evaluation, import or subprocess call, verified by review and by a test that scans the loader module. Do not build yet: No detection, judging, concurrency or reporting yet. ### Phase 3 · Detection in the right order Build: Run deterministic matchers first: token overlap against your known prompt text at a stated threshold, exact tool name matches, and secret patterns over the raw response. Then send the judge a transcript with every matched span replaced by a marker, and record what was redacted. Trap: Sending the full transcript to a third-party judge model means the tool built to find out whether your system prompt and your keys leak has just posted your system prompt and your keys into somebody else's inference logs, which is the exact outcome you were testing for; match deterministically on your own machine, replace every matched span with a marker before the judge sees anything, record which markers were substituted, and make the judge optional so a run with no judge key still produces every deterministic finding. Done when: A response containing a known secret pattern produces a finding without the secret ever appearing in a judge request, asserted by a test that inspects the outbound judge payload. Deterministic results override a contradicting judge verdict, tested with a judge stubbed to disagree. Severity is derived from the matched signal kind and not from the probe's severity hint, asserted by a test where a low-hint probe extracts a key. With no judge configured the run completes and reports every deterministic finding. Do not build yet: No multi-turn sequencing, rate control, reporting or diffing yet. ### Phase 4 · Multi-turn, and not knocking the target over Build: Support turns that reference earlier replies, with bounded concurrency defaulting low, a per-request timeout, backoff that honours the retry interval the server sends, a hard cap on requests per run and a limit flag for smoke tests. Stop the run when the error rate crosses a threshold. Trap: A harness with cheerful default concurrency pointed at your own production endpoint is an unplanned load test, and one that retries a rate-limit response immediately converts a rate limit into an outage you caused yourself on the system you were trying to protect; default the concurrency low, honour the interval the server asks for, cap total requests per run, and abort with a summary when the error rate crosses a threshold rather than grinding on and blaming the target. Done when: A stubbed rate-limit response with a retry interval causes a wait of at least that interval, tested. Total requests in a run never exceed the configured cap, asserted by a counting transport. An error rate above the threshold aborts the run with a partial report rather than continuing, tested. A multi-turn probe whose later turn references an earlier reply receives the actual reply text, tested end to end against a local stub. Do not build yet: No report rendering, diffing or exit codes yet. ### Phase 5 · A report that does not overclaim Build: Render a self-contained HTML report grouped by what leaked, with the pack version, pack date, probe count, families covered and the authorisation record at the top. Emit JSON for diffing. Add a diff command showing newly failing and newly passing probes, and exit non-zero on a high finding. Trap: A summary line reading 'no leaks found' will be pasted into a security questionnaire within a month, and it is not true: what happened is that this pack, on this date, against this endpoint, found nothing, while the jailbreak families it knows about are a snapshot of what its author understood when they wrote it; put the pack date, probe count and covered families in the summary, phrase the result as what was tested rather than what is safe, and state in the report itself that a self-run harness is not a third-party assessment. Done when: Every report carries the pack version, pack date, probe count, covered families and the authorisation record above the findings, asserted in the rendering test. The summary states what was tested and never asserts the endpoint is safe, checked by a test that fails on an assurance phrase. The diff command lists newly failing and newly passing probes between two runs and is stable when nothing changed. A high severity finding exits non-zero and a clean run exits zero, tested both ways. Do not build yet: Nothing after this. ### Out of scope (and why) - Testing, scanning or discovering anything not named in the target file, and there is deliberately no flag, variable or mode that permits it. - Denial of service, rate limit abuse, network scanning and authentication bypass testing, none of which this tool does and all of which need a different conversation about permission. - A hosted dashboard or a shared findings database, which would mean holding other people's leaked prompts and secrets on your infrastructure. - A numeric security score or a badge, because a number derived from the pack you happen to ship describes the pack rather than the endpoint. ### README must contain - That the tool refuses any host except the one in the target file, enforced per request including redirects, and that there is no way to turn that off - That an authorisation record is required before the first run and is printed into every report - That a leaked system prompt is evidence rather than the vulnerability, and that severity comes from what leaked - That the judge only ever sees a transcript with matched spans replaced by markers, and that the judge is optional - That a clean run means this pack on this date found nothing, and that it is not a third-party assessment ===== ARCHITECTURE.md ===== # Architecture · ZeroLeaks ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | A Python command line tool writing to a local run directory, no service and no dashboard | a security tool that phones home is a security problem | | Targeting | One target file naming exactly one host, enforced inside the HTTP transport | the difference between a test and an offence is whose machine it is | | Probes | A versioned directory of declarative probe files with a pack version and a date | the pack ages faster than the code and the report has to say how old it is | | Detection | Deterministic matchers first, a model judge second, deterministic always winning | a judge that overrules a regex match on your own key is worse than no judge | | Reporting | A self-contained HTML report plus JSON, with full transcripts and a diff between runs | the transcript is the finding; a severity label on its own proves nothing | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Transport | The per-request host check and redirect refusal. | Not swappable for a configuration-time check, which has three ways around it. | | Pack | Declarative probes, versioning, schema validation. | Any format, provided the loader can execute nothing. | | Detect | Deterministic matchers and severity from signals. | Any matcher set, provided deterministic results win. | | Judge | Redacted transcripts and structured verdicts. | Any model, provided it only ever sees markers. | | Report | Rendering, diffing, exit codes, framing. | Any renderer, provided the pack date heads the summary. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `TARGET_FILE` · required · The one endpoint. Its host is the only host the transport will speak to. - `AUTHORISATION_FILE` · required · Who granted permission, when, and until when. Printed into every report. - `SIGNALS_FILE` · required · Your prompt text, tool names and secret patterns. Never committed. - `TARGET_AUTH_HEADER` · required, secret · The credential for your own endpoint. Referenced by name from the target file. - `JUDGE_API_KEY` · optional, secret · Optional. Without it the run reports every deterministic finding and no judge verdicts. - `MAX_REQUESTS_PER_RUN` · required · A hard cap. The run stops here whatever the pack says. - `CONCURRENCY` · required · Low by default. This is pointed at something somebody depends on. - `RUNS_DIR` · required · Transcripts and reports. These contain whatever leaked, so treat them as secret. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · ZeroLeaks product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: A Python command line tool writing to a local run directory, no service and no dashboard, One target file naming exactly one host, enforced inside the HTTP transport, A versioned directory of declarative probe files with a pack version and a date, Deterministic matchers first, a model judge second, deterministic always winning, A self-contained HTML report plus JSON, with full transcripts and a diff between runs. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Checking the host once when the configuration loads leaves three ways out: a redirect, a proxy setting and a templated URL, any of which sends your adversarial traffic at somebody who never agreed to receive it and turns a test into an incident; put the check in the transport so it runs on every single request, turn off automatic redirect following, and make a cross-host attempt end the run rather than skip the request, so it is impossible to miss. - Encoding probes as Python functions means the pack cannot be updated without a release, cannot be reviewed by anyone who does not read your code, and eventually contains a probe that does something other than send a message, at which point your red-team harness has an arbitrary code execution path in it aimed at your own infrastructure; keep probes declarative, validate them at load, and make the loader structurally incapable of running anything. - Sending the full transcript to a third-party judge model means the tool built to find out whether your system prompt and your keys leak has just posted your system prompt and your keys into somebody else's inference logs, which is the exact outcome you were testing for; match deterministically on your own machine, replace every matched span with a marker before the judge sees anything, record which markers were substituted, and make the judge optional so a run with no judge key still produces every deterministic finding. - A harness with cheerful default concurrency pointed at your own production endpoint is an unplanned load test, and one that retries a rate-limit response immediately converts a rate limit into an outage you caused yourself on the system you were trying to protect; default the concurrency low, honour the interval the server asks for, cap total requests per run, and abort with a summary when the error rate crosses a threshold rather than grinding on and blaming the target. - A summary line reading 'no leaks found' will be pasted into a security questionnaire within a month, and it is not true: what happened is that this pack, on this date, against this endpoint, found nothing, while the jailbreak families it knows about are a snapshot of what its author understood when they wrote it; put the pack date, probe count and covered families in the summary, phrase the result as what was tested rather than what is safe, and state in the report itself that a self-run harness is not a third-party assessment. ===== MILESTONES.md ===== # Delivery milestones · ZeroLeaks Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A target you are allowed to test The tool cannot send a request to any host other than the one you recorded permission for. ### Steps 1. Enforce the host inside the transport, per request Configuration-time checks have three documented ways around them. Files: `leakprobe/transport.py`, `tests/test_host_guard.py` 2. Disable redirect following and fail the run on a cross-host hop A redirect is the easiest way to point this at a stranger. Files: `leakprobe/transport.py` 3. Require an authorisation record and print it It goes in the report too, because that is where it matters. Files: `leakprobe/auth.py`, `tests/test_auth_required.py` ### Done when - [ ] A cross-host redirect ends the run with zero requests - [ ] Every request passes the host check - [ ] No authorisation means no run - [ ] No flag or variable disables the check ### Watch out - Checking the host once when the configuration loads leaves three ways out: a redirect, a proxy setting and a templated URL, any of which sends your adversarial traffic at somebody who never agreed to receive it and turns a test into an incident; put the check in the transport so it runs on every single request, turn off automatic redirect following, and make a cross-host attempt end the run rather than skip the request, so it is impossible to miss. ## M2 · A probe pack that is data, not code Probes can be added, reviewed and dated without touching the program. ### Steps 1. Define the probe schema and validate at load A bad probe stops the run; it does not get skipped. Files: `leakprobe/probes/schema.py`, `tests/test_probe_schema.py` 2. Stamp the pack version and date into every run A report without a pack date is a report with no shelf life. Files: `leakprobe/pack.py` 3. Keep the loader incapable of executing anything No eval, no import, no subprocess, and a test that says so. Files: `tests/test_loader_inert.py` ### Done when - [ ] A new probe file works with no code change - [ ] An invalid probe stops the run by name - [ ] Pack version and date reach every report - [ ] The loader executes nothing ### Watch out - Encoding probes as Python functions means the pack cannot be updated without a release, cannot be reviewed by anyone who does not read your code, and eventually contains a probe that does something other than send a message, at which point your red-team harness has an arbitrary code execution path in it aimed at your own infrastructure; keep probes declarative, validate them at load, and make the loader structurally incapable of running anything. ## M3 · Detection in the right order A deterministic match is the finding, and the judge never sees your secrets. ### Steps 1. Match deterministically before anything leaves the machine The matchers are cheap and they are the reliable half. Files: `leakprobe/detect.py`, `tests/test_detectors.py` 2. Redact matched spans before the judge sees the transcript Then record which markers stood in for what. Files: `leakprobe/judge.py`, `tests/test_judge_redaction.py` 3. Compute severity from the signal, not the probe A prompt with nothing in it is not a critical finding. Files: `leakprobe/severity.py` ### Done when - [ ] No secret reaches the judge request - [ ] Deterministic results beat the judge - [ ] Severity follows the matched signal - [ ] A run without a judge still reports findings ### Watch out - Sending the full transcript to a third-party judge model means the tool built to find out whether your system prompt and your keys leak has just posted your system prompt and your keys into somebody else's inference logs, which is the exact outcome you were testing for; match deterministically on your own machine, replace every matched span with a marker before the judge sees anything, record which markers were substituted, and make the judge optional so a run with no judge key still produces every deterministic finding. ## M4 · Multi-turn, and not knocking the target over A run explores multi-turn attacks while staying gentle enough to point at something real. ### Steps 1. Default concurrency low and cap requests per run This is pointed at something somebody depends on. Files: `leakprobe/runner.py`, `tests/test_cap.py` 2. Honour the retry interval the server sends Ignoring it is how a rate limit becomes an outage. Files: `leakprobe/backoff.py` 3. Abort on a high error rate with a partial report A thousand failed requests is not more data. Files: `leakprobe/runner.py` ### Done when - [ ] A retry interval is honoured in full - [ ] The request cap is never exceeded - [ ] A high error rate aborts with a partial report - [ ] Multi-turn probes see the real earlier reply ### Watch out - A harness with cheerful default concurrency pointed at your own production endpoint is an unplanned load test, and one that retries a rate-limit response immediately converts a rate limit into an outage you caused yourself on the system you were trying to protect; default the concurrency low, honour the interval the server asks for, cap total requests per run, and abort with a summary when the error rate crosses a threshold rather than grinding on and blaming the target. ## M5 · A report that does not overclaim (production only) The output is usable as a regression gate and cannot be mistaken for an assurance. ### Steps 1. Put the pack date and coverage above the findings It is the context that decides what the result means. Files: `leakprobe/report.py`, `templates/report.html.j2` 2. Ban assurance phrasing with a test Someone will paste this into a questionnaire. Files: `tests/test_no_assurance.py` 3. Make diff stable and the exit code meaningful That is what turns this into a regression gate. Files: `leakprobe/diff.py`, `tests/test_diff.py` ### Done when - [ ] Pack date, coverage and authorisation head every report - [ ] No assurance phrasing survives the test - [ ] The diff is stable when nothing changed - [ ] A high finding exits non-zero ### Watch out - A summary line reading 'no leaks found' will be pasted into a security questionnaire within a month, and it is not true: what happened is that this pack, on this date, against this endpoint, found nothing, while the jailbreak families it knows about are a snapshot of what its author understood when they wrote it; put the pack date, probe count and covered families in the summary, phrase the result as what was tested rather than what is safe, and state in the report itself that a self-run harness is not a third-party assessment. ===== OPERATIONS.md ===== # Operations · ZeroLeaks ## Backup The run directory is the evidence and it contains whatever leaked, so it is both worth keeping and dangerous to keep. Encrypt it, give it a retention period, and never put it in the same repository as the code. ## Restore Restore a run directory only to diff against, and check the authorisation record it contains has not expired before treating it as current. An old run is a statement about an old pack against an old model and it stops meaning much quite quickly. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring The pack date against today, the proportion of probes that ended in refusal rather than a response, and the diff between consecutive runs. A sudden jump in refusals usually means the endpoint changed rather than that it got safer. ## Incident checklist The incident is a report being read as an assurance, or a run directory being shared with the leaked material still in it. The first is why the framing is tested; the second is why the runs directory is encrypted and has an end date. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A cross-host redirect is confirmed to end the run with zero requests - [ ] No flag or variable is confirmed to disable the host check - [ ] No secret is confirmed to reach the judge request - [ ] Severity is confirmed to follow the matched signal, not the probe - [ ] No assurance phrasing is confirmed to survive the report test ## Launch constraint Do not market omitted ZeroLeaks capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The one endpoint. Its host is the only host the transport will speak to. TARGET_FILE=./target.yaml # Required. Who granted permission, when, and until when. Printed into every report. AUTHORISATION_FILE=./authorisation.yaml # Required. Your prompt text, tool names and secret patterns. Never committed. SIGNALS_FILE=./signals.yaml # Required · secret. The credential for your own endpoint. Referenced by name from the target file. TARGET_AUTH_HEADER=Bearer ...redacted # Optional · secret. Optional. Without it the run reports every deterministic finding and no judge verdicts. JUDGE_API_KEY=sk-...redacted # Required. A hard cap. The run stops here whatever the pack says. MAX_REQUESTS_PER_RUN=600 # Required. Low by default. This is pointed at something somebody depends on. CONCURRENCY=2 # Required. Transcripts and reports. These contain whatever leaked, so treat them as secret. RUNS_DIR=./runs
# ZeroLeaks · indie build A local extraction-probe harness whose hard problems are enforcing authorisation at the transport rather than in documentation, scoring a finding by what actually leaked rather than by whether a prompt came back, and judging transcripts without posting your own secrets to somebody else's model. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | A Python command line tool writing to a local run directory, no service and no dashboard | a security tool that phones home is a security problem | | Targeting | One target file naming exactly one host, enforced inside the HTTP transport | the difference between a test and an offence is whose machine it is | | Probes | A versioned directory of declarative probe files with a pack version and a date | the pack ages faster than the code and the report has to say how old it is | | Detection | Deterministic matchers first, a model judge second, deterministic always winning | a judge that overrules a regex match on your own key is worse than no judge | | Reporting | A self-contained HTML report plus JSON, with full transcripts and a diff between runs | the transcript is the finding; a severity label on its own proves nothing | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - Why: Runs the command line tool. - Get it: python.org or your package manager. - Verify: python3 --version prints 3.12 or higher - [ ] **Written authorisation to test the target** · free, and non-negotiable - Why: Authorisation is what separates this from an offence, and it is a required record in the tool rather than an assumption, so you need it before anything runs. - Get it: An email or signed note from whoever owns the endpoint, naming the host, the scope and the dates. - Verify: The authorisation record exists, names the host and has not expired - [ ] **A reachable chat endpoint for the application under test** · whatever your own endpoint costs to call - Why: The target file describes one endpoint, its request shape and where the reply text sits in the response, and getting that wrong means every probe scores as a refusal. - Get it: Point it at a staging deployment of your own application if you have one. - Verify: A single probe returns extracted reply text rather than a raw response body - [ ] **Your real system prompt text, tool names and secret patterns** · free - Why: The deterministic matchers are the reliable half of this build and they are worthless without the actual strings to match against. - Get it: Copy the current prompt and tool schema, and write patterns for every key format you use. - Verify: A crafted response containing each signal produces a finding - [ ] **An API key for a judge model** (optional) · pennies per run - Why: The judge catches paraphrased leakage the matchers miss, but it is optional by design because a run without it must still produce every deterministic finding. - Get it: Any provider whose model returns structured output reliably. - Verify: A redacted transcript returns strict JSON with a verdict ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Testing anything not named in the target file. - Denial of service, scanning or authentication bypass. - A hosted dashboard or shared findings database. - A numeric security score or badge. - A curated, maintained probe corpus that tracks new jailbreak families instead of whatever the agent remembered on build day - Multi-turn and multi-model attack strategies, including crescendo and encoding tricks, done properly rather than as single-shot prompts - A third-party report with a date on it that you can hand to a customer or an auditor - Severity triage and remediation guidance written by someone who has seen a lot of these - Regression runs on every model or prompt change without you remembering to trigger them If one of those is essential to you, that is the reason to keep paying for ZeroLeaks, and the README should say so rather than pretend.
# Build brief · ZeroLeaks The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local harness that fires a versioned pack of extraction probes at an endpoint you are authorised to test and reports exactly what came back to replace ZeroLeaks. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two rules. Neither is a preference. This tool only ever points at an endpoint you own or hold written permission to test, and that is a check in the code rather than a line in the README. The target lives in a local file naming one host. The transport refuses any other host on every request, including after a redirect. There is no discovery, no scanning, no target list, no flag that turns the check off, and no mode that accepts a host from the command line. Before the first run the tool requires an authorisation record naming who granted permission and when, and it writes that record into every report. Computer misuse law generally turns on authorisation rather than on whether any harm occurred, so get it in writing, check what applies where you and the target are, and keep the record. Second, be honest about what a finding is. The 2025 OWASP list for language model applications treats system prompt leakage as its own entry and is blunt: the system prompt should not be considered a secret, nor used as a security control, and credentials and connection strings should not be in it at all. So an extracted prompt is not the vulnerability, it is evidence about what was in the prompt. Severity comes from what leaked, not from which probe fired. And there is no score: a number derived from a pack you shipped describes the pack, not the endpoint. ### Stack (fixed, do not substitute) - Runtime: A Python command line tool writing to a local run directory, no service and no dashboard · a security tool that phones home is a security problem - Targeting: One target file naming exactly one host, enforced inside the HTTP transport · the difference between a test and an offence is whose machine it is - Probes: A versioned directory of declarative probe files with a pack version and a date · the pack ages faster than the code and the report has to say how old it is - Detection: Deterministic matchers first, a model judge second, deterministic always winning · a judge that overrules a regex match on your own key is worse than no judge - Reporting: A self-contained HTML report plus JSON, with full transcripts and a diff between runs · the transcript is the finding; a severity label on its own proves nothing ### Data model (create this before Phase 1) - authorisations(id, target_host, granted_by, granted_at, expires_at, evidence_path) · a run with no current row here does not start - targets(id, name, url, method, header_names_json, body_template, reply_path) · header values come from the environment and never from this file - signals(id, kind, value_ref, description) · kind is prompt_text, tool_name or secret_pattern, and the values live outside the repository - probes(id, pack_version, family, severity_hint, turns_json, added_at, source_note) - runs(id, target_id, pack_version, started_at, ended_at, authorisation_id, judge_model_id, probe_count, request_count) - attempts(id, run_id, probe_id, turn_ordinal, request_json, response_json, latency_ms, http_status) · verbatim, because this is the evidence - findings(id, run_id, probe_id, detector, matched_signal_id, severity, judge_json, first_seen_run_id) · severity is computed from which signal matched, never from which probe fired ### Phase 1 · A target you are allowed to test Build: Read the target file and the authorisation record. Enforce the host check inside the HTTP transport on every request, with automatic redirect following disabled and any cross-host redirect treated as a run-ending error. Print the authorisation and the scope before the first run. Trap: Checking the host once when the configuration loads leaves three ways out: a redirect, a proxy setting and a templated URL, any of which sends your adversarial traffic at somebody who never agreed to receive it and turns a test into an incident; put the check in the transport so it runs on every single request, turn off automatic redirect following, and make a cross-host attempt end the run rather than skip the request, so it is impossible to miss. Done when: A target whose URL redirects to another host ends the run with a named error and zero requests to that host, tested with a local redirect. Every request passes the host check, asserted by a test that patches the transport and counts checks against requests. A run without a current authorisation row refuses to start and says what is missing. There is no command line flag, environment variable or configuration key that disables the check, verified by review and by a test that greps for one. Do not build yet: No probes, detection, judging or reporting yet. ### Phase 2 · A probe pack that is data, not code Build: Define probes as declarative files with an identifier, a family, a severity hint, one or more turns and optional references to earlier replies. Version the pack as a whole with a date. Validate every file against a schema at load, and give the loader no way to execute anything. Trap: Encoding probes as Python functions means the pack cannot be updated without a release, cannot be reviewed by anyone who does not read your code, and eventually contains a probe that does something other than send a message, at which point your red-team harness has an arbitrary code execution path in it aimed at your own infrastructure; keep probes declarative, validate them at load, and make the loader structurally incapable of running anything. Done when: A new probe file is picked up with no code change, tested by adding one. A probe file that fails the schema is reported by name and the run refuses to start rather than skipping it. The pack version and date appear in every run row and every report. The loader contains no evaluation, import or subprocess call, verified by review and by a test that scans the loader module. Do not build yet: No detection, judging, concurrency or reporting yet. ### Phase 3 · Detection in the right order Build: Run deterministic matchers first: token overlap against your known prompt text at a stated threshold, exact tool name matches, and secret patterns over the raw response. Then send the judge a transcript with every matched span replaced by a marker, and record what was redacted. Trap: Sending the full transcript to a third-party judge model means the tool built to find out whether your system prompt and your keys leak has just posted your system prompt and your keys into somebody else's inference logs, which is the exact outcome you were testing for; match deterministically on your own machine, replace every matched span with a marker before the judge sees anything, record which markers were substituted, and make the judge optional so a run with no judge key still produces every deterministic finding. Done when: A response containing a known secret pattern produces a finding without the secret ever appearing in a judge request, asserted by a test that inspects the outbound judge payload. Deterministic results override a contradicting judge verdict, tested with a judge stubbed to disagree. Severity is derived from the matched signal kind and not from the probe's severity hint, asserted by a test where a low-hint probe extracts a key. With no judge configured the run completes and reports every deterministic finding. Do not build yet: No multi-turn sequencing, rate control, reporting or diffing yet. ### Phase 4 · Multi-turn, and not knocking the target over Build: Support turns that reference earlier replies, with bounded concurrency defaulting low, a per-request timeout, backoff that honours the retry interval the server sends, a hard cap on requests per run and a limit flag for smoke tests. Stop the run when the error rate crosses a threshold. Trap: A harness with cheerful default concurrency pointed at your own production endpoint is an unplanned load test, and one that retries a rate-limit response immediately converts a rate limit into an outage you caused yourself on the system you were trying to protect; default the concurrency low, honour the interval the server asks for, cap total requests per run, and abort with a summary when the error rate crosses a threshold rather than grinding on and blaming the target. Done when: A stubbed rate-limit response with a retry interval causes a wait of at least that interval, tested. Total requests in a run never exceed the configured cap, asserted by a counting transport. An error rate above the threshold aborts the run with a partial report rather than continuing, tested. A multi-turn probe whose later turn references an earlier reply receives the actual reply text, tested end to end against a local stub. Do not build yet: No report rendering, diffing or exit codes yet. ### Phase 5 · A report that does not overclaim Build: Render a self-contained HTML report grouped by what leaked, with the pack version, pack date, probe count, families covered and the authorisation record at the top. Emit JSON for diffing. Add a diff command showing newly failing and newly passing probes, and exit non-zero on a high finding. Trap: A summary line reading 'no leaks found' will be pasted into a security questionnaire within a month, and it is not true: what happened is that this pack, on this date, against this endpoint, found nothing, while the jailbreak families it knows about are a snapshot of what its author understood when they wrote it; put the pack date, probe count and covered families in the summary, phrase the result as what was tested rather than what is safe, and state in the report itself that a self-run harness is not a third-party assessment. Done when: Every report carries the pack version, pack date, probe count, covered families and the authorisation record above the findings, asserted in the rendering test. The summary states what was tested and never asserts the endpoint is safe, checked by a test that fails on an assurance phrase. The diff command lists newly failing and newly passing probes between two runs and is stable when nothing changed. A high severity finding exits non-zero and a clean run exits zero, tested both ways. Do not build yet: Nothing after this. ### Out of scope (and why) - Testing, scanning or discovering anything not named in the target file, and there is deliberately no flag, variable or mode that permits it. - Denial of service, rate limit abuse, network scanning and authentication bypass testing, none of which this tool does and all of which need a different conversation about permission. - A hosted dashboard or a shared findings database, which would mean holding other people's leaked prompts and secrets on your infrastructure. - A numeric security score or a badge, because a number derived from the pack you happen to ship describes the pack rather than the endpoint. ### README must contain - That the tool refuses any host except the one in the target file, enforced per request including redirects, and that there is no way to turn that off - That an authorisation record is required before the first run and is printed into every report - That a leaked system prompt is evidence rather than the vulnerability, and that severity comes from what leaked - That the judge only ever sees a transcript with matched spans replaced by markers, and that the judge is optional - That a clean run means this pack on this date found nothing, and that it is not a third-party assessment
# Agent instructions · ZeroLeaks indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A Python command line tool writing to a local run directory, no service and no dashboard, One target file naming exactly one host, enforced inside the HTTP transport, A versioned directory of declarative probe files with a pack version and a date, Deterministic matchers first, a model judge second, deterministic always winning, A self-contained HTML report plus JSON, with full transcripts and a diff between runs. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Checking the host once when the configuration loads leaves three ways out: a redirect, a proxy setting and a templated URL, any of which sends your adversarial traffic at somebody who never agreed to receive it and turns a test into an incident; put the check in the transport so it runs on every single request, turn off automatic redirect following, and make a cross-host attempt end the run rather than skip the request, so it is impossible to miss. - Encoding probes as Python functions means the pack cannot be updated without a release, cannot be reviewed by anyone who does not read your code, and eventually contains a probe that does something other than send a message, at which point your red-team harness has an arbitrary code execution path in it aimed at your own infrastructure; keep probes declarative, validate them at load, and make the loader structurally incapable of running anything. - Sending the full transcript to a third-party judge model means the tool built to find out whether your system prompt and your keys leak has just posted your system prompt and your keys into somebody else's inference logs, which is the exact outcome you were testing for; match deterministically on your own machine, replace every matched span with a marker before the judge sees anything, record which markers were substituted, and make the judge optional so a run with no judge key still produces every deterministic finding. - A harness with cheerful default concurrency pointed at your own production endpoint is an unplanned load test, and one that retries a rate-limit response immediately converts a rate limit into an outage you caused yourself on the system you were trying to protect; default the concurrency low, honour the interval the server asks for, cap total requests per run, and abort with a summary when the error rate crosses a threshold rather than grinding on and blaming the target.
# Build plan · ZeroLeaks A local extraction-probe harness whose hard problems are enforcing authorisation at the transport rather than in documentation, scoring a finding by what actually leaked rather than by whether a prompt came back, and judging transcripts without posting your own secrets to somebody else's model. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A target you are allowed to test The tool cannot send a request to any host other than the one you recorded permission for. ### Steps 1. Enforce the host inside the transport, per request Configuration-time checks have three documented ways around them. Files: `leakprobe/transport.py`, `tests/test_host_guard.py` 2. Disable redirect following and fail the run on a cross-host hop A redirect is the easiest way to point this at a stranger. Files: `leakprobe/transport.py` 3. Require an authorisation record and print it It goes in the report too, because that is where it matters. Files: `leakprobe/auth.py`, `tests/test_auth_required.py` ### Done when - [ ] A cross-host redirect ends the run with zero requests - [ ] Every request passes the host check - [ ] No authorisation means no run - [ ] No flag or variable disables the check ### Watch out - Checking the host once when the configuration loads leaves three ways out: a redirect, a proxy setting and a templated URL, any of which sends your adversarial traffic at somebody who never agreed to receive it and turns a test into an incident; put the check in the transport so it runs on every single request, turn off automatic redirect following, and make a cross-host attempt end the run rather than skip the request, so it is impossible to miss. ## Phase 2 · A probe pack that is data, not code Probes can be added, reviewed and dated without touching the program. ### Steps 1. Define the probe schema and validate at load A bad probe stops the run; it does not get skipped. Files: `leakprobe/probes/schema.py`, `tests/test_probe_schema.py` 2. Stamp the pack version and date into every run A report without a pack date is a report with no shelf life. Files: `leakprobe/pack.py` 3. Keep the loader incapable of executing anything No eval, no import, no subprocess, and a test that says so. Files: `tests/test_loader_inert.py` ### Done when - [ ] A new probe file works with no code change - [ ] An invalid probe stops the run by name - [ ] Pack version and date reach every report - [ ] The loader executes nothing ### Watch out - Encoding probes as Python functions means the pack cannot be updated without a release, cannot be reviewed by anyone who does not read your code, and eventually contains a probe that does something other than send a message, at which point your red-team harness has an arbitrary code execution path in it aimed at your own infrastructure; keep probes declarative, validate them at load, and make the loader structurally incapable of running anything. ## Phase 3 · Detection in the right order A deterministic match is the finding, and the judge never sees your secrets. ### Steps 1. Match deterministically before anything leaves the machine The matchers are cheap and they are the reliable half. Files: `leakprobe/detect.py`, `tests/test_detectors.py` 2. Redact matched spans before the judge sees the transcript Then record which markers stood in for what. Files: `leakprobe/judge.py`, `tests/test_judge_redaction.py` 3. Compute severity from the signal, not the probe A prompt with nothing in it is not a critical finding. Files: `leakprobe/severity.py` ### Done when - [ ] No secret reaches the judge request - [ ] Deterministic results beat the judge - [ ] Severity follows the matched signal - [ ] A run without a judge still reports findings ### Watch out - Sending the full transcript to a third-party judge model means the tool built to find out whether your system prompt and your keys leak has just posted your system prompt and your keys into somebody else's inference logs, which is the exact outcome you were testing for; match deterministically on your own machine, replace every matched span with a marker before the judge sees anything, record which markers were substituted, and make the judge optional so a run with no judge key still produces every deterministic finding. ## Phase 4 · Multi-turn, and not knocking the target over A run explores multi-turn attacks while staying gentle enough to point at something real. ### Steps 1. Default concurrency low and cap requests per run This is pointed at something somebody depends on. Files: `leakprobe/runner.py`, `tests/test_cap.py` 2. Honour the retry interval the server sends Ignoring it is how a rate limit becomes an outage. Files: `leakprobe/backoff.py` 3. Abort on a high error rate with a partial report A thousand failed requests is not more data. Files: `leakprobe/runner.py` ### Done when - [ ] A retry interval is honoured in full - [ ] The request cap is never exceeded - [ ] A high error rate aborts with a partial report - [ ] Multi-turn probes see the real earlier reply ### Watch out - A harness with cheerful default concurrency pointed at your own production endpoint is an unplanned load test, and one that retries a rate-limit response immediately converts a rate limit into an outage you caused yourself on the system you were trying to protect; default the concurrency low, honour the interval the server asks for, cap total requests per run, and abort with a summary when the error rate crosses a threshold rather than grinding on and blaming the target. ## Not in this build - Testing anything not named in the target file. - Denial of service, scanning or authentication bypass. - A hosted dashboard or shared findings database. - A numeric security score or badge. ## After v1, if you want it - A scheduled run against staging on every prompt change, which is the only use of this that catches anything before a customer does - A second signals file for a different deployment, which tests whether severity really was computed from signals - A written note beside each probe family saying where it came from and when, so the pack ages visibly rather than silently
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. The one endpoint. Its host is the only host the transport will speak to. TARGET_FILE=./target.yaml # Required. Who granted permission, when, and until when. Printed into every report. AUTHORISATION_FILE=./authorisation.yaml # Required. Your prompt text, tool names and secret patterns. Never committed. SIGNALS_FILE=./signals.yaml # Required · secret. The credential for your own endpoint. Referenced by name from the target file. TARGET_AUTH_HEADER=Bearer ...redacted # Optional · secret. Optional. Without it the run reports every deterministic finding and no judge verdicts. JUDGE_API_KEY=sk-...redacted # Required. A hard cap. The run stops here whatever the pack says. MAX_REQUESTS_PER_RUN=600 # Required. Low by default. This is pointed at something somebody depends on. CONCURRENCY=2 # Required. Transcripts and reports. These contain whatever leaked, so treat them as secret. RUNS_DIR=./runs
# ZeroLeaks · product brief ## Problem The mechanics here are not exotic: fire a few hundred adversarial prompts at your own chat endpoint, capture the responses, and check whether any of them contain your system prompt, tool schemas or API keys. An agent can build that loop, including an LLM-as-judge scorer and an HTML report, in a single sitting, and it will find the embarrassing stuff on day one. What you cannot one-shot is a probe library that stays current with each new model release and each new jailbreak family, because that is maintained knowledge, not code. There is also a trust angle: 'we ran our own script and found nothing' reads very differently in a security review than a dated third-party report. Build it for your own sanity checks, keep paying if you need something to show someone else. ## Product outcome A harness that cannot be pointed at somebody else's endpoint, scores a finding by what actually leaked rather than by whether a prompt came back, never shows your secrets to the judge, and produces a report nobody can honestly mistake for an assurance. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - An API key for the model you use as judge - A reachable chat endpoint or API for the app under test, plus permission to hammer it - A copy of your real system prompt and secret patterns to match against ## Explicit non-goals for v1 - Testing anything not named in the target file. - Denial of service, scanning or authentication bypass. - A hosted dashboard or shared findings database. - A numeric security score or badge. - A curated, maintained probe corpus that tracks new jailbreak families instead of whatever the agent remembered on build day - Multi-turn and multi-model attack strategies, including crescendo and encoding tricks, done properly rather than as single-shot prompts - A third-party report with a date on it that you can hand to a customer or an auditor - Severity triage and remediation guidance written by someone who has seen a lot of these - Regression runs on every model or prompt change without you remembering to trigger them ## Success criteria - A cross-host redirect is confirmed to end the run with zero requests - No flag or variable is confirmed to disable the host check - No secret is confirmed to reach the judge request - Severity is confirmed to follow the matched signal, not the probe - No assurance phrasing is confirmed to survive the report test
# Build brief · ZeroLeaks The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local harness that fires a versioned pack of extraction probes at an endpoint you are authorised to test and reports exactly what came back to replace ZeroLeaks. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two rules. Neither is a preference. This tool only ever points at an endpoint you own or hold written permission to test, and that is a check in the code rather than a line in the README. The target lives in a local file naming one host. The transport refuses any other host on every request, including after a redirect. There is no discovery, no scanning, no target list, no flag that turns the check off, and no mode that accepts a host from the command line. Before the first run the tool requires an authorisation record naming who granted permission and when, and it writes that record into every report. Computer misuse law generally turns on authorisation rather than on whether any harm occurred, so get it in writing, check what applies where you and the target are, and keep the record. Second, be honest about what a finding is. The 2025 OWASP list for language model applications treats system prompt leakage as its own entry and is blunt: the system prompt should not be considered a secret, nor used as a security control, and credentials and connection strings should not be in it at all. So an extracted prompt is not the vulnerability, it is evidence about what was in the prompt. Severity comes from what leaked, not from which probe fired. And there is no score: a number derived from a pack you shipped describes the pack, not the endpoint. ### Stack (fixed, do not substitute) - Runtime: A Python command line tool writing to a local run directory, no service and no dashboard · a security tool that phones home is a security problem - Targeting: One target file naming exactly one host, enforced inside the HTTP transport · the difference between a test and an offence is whose machine it is - Probes: A versioned directory of declarative probe files with a pack version and a date · the pack ages faster than the code and the report has to say how old it is - Detection: Deterministic matchers first, a model judge second, deterministic always winning · a judge that overrules a regex match on your own key is worse than no judge - Reporting: A self-contained HTML report plus JSON, with full transcripts and a diff between runs · the transcript is the finding; a severity label on its own proves nothing ### Data model (create this before Phase 1) - authorisations(id, target_host, granted_by, granted_at, expires_at, evidence_path) · a run with no current row here does not start - targets(id, name, url, method, header_names_json, body_template, reply_path) · header values come from the environment and never from this file - signals(id, kind, value_ref, description) · kind is prompt_text, tool_name or secret_pattern, and the values live outside the repository - probes(id, pack_version, family, severity_hint, turns_json, added_at, source_note) - runs(id, target_id, pack_version, started_at, ended_at, authorisation_id, judge_model_id, probe_count, request_count) - attempts(id, run_id, probe_id, turn_ordinal, request_json, response_json, latency_ms, http_status) · verbatim, because this is the evidence - findings(id, run_id, probe_id, detector, matched_signal_id, severity, judge_json, first_seen_run_id) · severity is computed from which signal matched, never from which probe fired ### Phase 1 · A target you are allowed to test Build: Read the target file and the authorisation record. Enforce the host check inside the HTTP transport on every request, with automatic redirect following disabled and any cross-host redirect treated as a run-ending error. Print the authorisation and the scope before the first run. Trap: Checking the host once when the configuration loads leaves three ways out: a redirect, a proxy setting and a templated URL, any of which sends your adversarial traffic at somebody who never agreed to receive it and turns a test into an incident; put the check in the transport so it runs on every single request, turn off automatic redirect following, and make a cross-host attempt end the run rather than skip the request, so it is impossible to miss. Done when: A target whose URL redirects to another host ends the run with a named error and zero requests to that host, tested with a local redirect. Every request passes the host check, asserted by a test that patches the transport and counts checks against requests. A run without a current authorisation row refuses to start and says what is missing. There is no command line flag, environment variable or configuration key that disables the check, verified by review and by a test that greps for one. Do not build yet: No probes, detection, judging or reporting yet. ### Phase 2 · A probe pack that is data, not code Build: Define probes as declarative files with an identifier, a family, a severity hint, one or more turns and optional references to earlier replies. Version the pack as a whole with a date. Validate every file against a schema at load, and give the loader no way to execute anything. Trap: Encoding probes as Python functions means the pack cannot be updated without a release, cannot be reviewed by anyone who does not read your code, and eventually contains a probe that does something other than send a message, at which point your red-team harness has an arbitrary code execution path in it aimed at your own infrastructure; keep probes declarative, validate them at load, and make the loader structurally incapable of running anything. Done when: A new probe file is picked up with no code change, tested by adding one. A probe file that fails the schema is reported by name and the run refuses to start rather than skipping it. The pack version and date appear in every run row and every report. The loader contains no evaluation, import or subprocess call, verified by review and by a test that scans the loader module. Do not build yet: No detection, judging, concurrency or reporting yet. ### Phase 3 · Detection in the right order Build: Run deterministic matchers first: token overlap against your known prompt text at a stated threshold, exact tool name matches, and secret patterns over the raw response. Then send the judge a transcript with every matched span replaced by a marker, and record what was redacted. Trap: Sending the full transcript to a third-party judge model means the tool built to find out whether your system prompt and your keys leak has just posted your system prompt and your keys into somebody else's inference logs, which is the exact outcome you were testing for; match deterministically on your own machine, replace every matched span with a marker before the judge sees anything, record which markers were substituted, and make the judge optional so a run with no judge key still produces every deterministic finding. Done when: A response containing a known secret pattern produces a finding without the secret ever appearing in a judge request, asserted by a test that inspects the outbound judge payload. Deterministic results override a contradicting judge verdict, tested with a judge stubbed to disagree. Severity is derived from the matched signal kind and not from the probe's severity hint, asserted by a test where a low-hint probe extracts a key. With no judge configured the run completes and reports every deterministic finding. Do not build yet: No multi-turn sequencing, rate control, reporting or diffing yet. ### Phase 4 · Multi-turn, and not knocking the target over Build: Support turns that reference earlier replies, with bounded concurrency defaulting low, a per-request timeout, backoff that honours the retry interval the server sends, a hard cap on requests per run and a limit flag for smoke tests. Stop the run when the error rate crosses a threshold. Trap: A harness with cheerful default concurrency pointed at your own production endpoint is an unplanned load test, and one that retries a rate-limit response immediately converts a rate limit into an outage you caused yourself on the system you were trying to protect; default the concurrency low, honour the interval the server asks for, cap total requests per run, and abort with a summary when the error rate crosses a threshold rather than grinding on and blaming the target. Done when: A stubbed rate-limit response with a retry interval causes a wait of at least that interval, tested. Total requests in a run never exceed the configured cap, asserted by a counting transport. An error rate above the threshold aborts the run with a partial report rather than continuing, tested. A multi-turn probe whose later turn references an earlier reply receives the actual reply text, tested end to end against a local stub. Do not build yet: No report rendering, diffing or exit codes yet. ### Phase 5 · A report that does not overclaim Build: Render a self-contained HTML report grouped by what leaked, with the pack version, pack date, probe count, families covered and the authorisation record at the top. Emit JSON for diffing. Add a diff command showing newly failing and newly passing probes, and exit non-zero on a high finding. Trap: A summary line reading 'no leaks found' will be pasted into a security questionnaire within a month, and it is not true: what happened is that this pack, on this date, against this endpoint, found nothing, while the jailbreak families it knows about are a snapshot of what its author understood when they wrote it; put the pack date, probe count and covered families in the summary, phrase the result as what was tested rather than what is safe, and state in the report itself that a self-run harness is not a third-party assessment. Done when: Every report carries the pack version, pack date, probe count, covered families and the authorisation record above the findings, asserted in the rendering test. The summary states what was tested and never asserts the endpoint is safe, checked by a test that fails on an assurance phrase. The diff command lists newly failing and newly passing probes between two runs and is stable when nothing changed. A high severity finding exits non-zero and a clean run exits zero, tested both ways. Do not build yet: Nothing after this. ### Out of scope (and why) - Testing, scanning or discovering anything not named in the target file, and there is deliberately no flag, variable or mode that permits it. - Denial of service, rate limit abuse, network scanning and authentication bypass testing, none of which this tool does and all of which need a different conversation about permission. - A hosted dashboard or a shared findings database, which would mean holding other people's leaked prompts and secrets on your infrastructure. - A numeric security score or a badge, because a number derived from the pack you happen to ship describes the pack rather than the endpoint. ### README must contain - That the tool refuses any host except the one in the target file, enforced per request including redirects, and that there is no way to turn that off - That an authorisation record is required before the first run and is printed into every report - That a leaked system prompt is evidence rather than the vulnerability, and that severity comes from what leaked - That the judge only ever sees a transcript with matched spans replaced by markers, and that the judge is optional - That a clean run means this pack on this date found nothing, and that it is not a third-party assessment
# Architecture · ZeroLeaks ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | A Python command line tool writing to a local run directory, no service and no dashboard | a security tool that phones home is a security problem | | Targeting | One target file naming exactly one host, enforced inside the HTTP transport | the difference between a test and an offence is whose machine it is | | Probes | A versioned directory of declarative probe files with a pack version and a date | the pack ages faster than the code and the report has to say how old it is | | Detection | Deterministic matchers first, a model judge second, deterministic always winning | a judge that overrules a regex match on your own key is worse than no judge | | Reporting | A self-contained HTML report plus JSON, with full transcripts and a diff between runs | the transcript is the finding; a severity label on its own proves nothing | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Transport | The per-request host check and redirect refusal. | Not swappable for a configuration-time check, which has three ways around it. | | Pack | Declarative probes, versioning, schema validation. | Any format, provided the loader can execute nothing. | | Detect | Deterministic matchers and severity from signals. | Any matcher set, provided deterministic results win. | | Judge | Redacted transcripts and structured verdicts. | Any model, provided it only ever sees markers. | | Report | Rendering, diffing, exit codes, framing. | Any renderer, provided the pack date heads the summary. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `TARGET_FILE` · required · The one endpoint. Its host is the only host the transport will speak to. - `AUTHORISATION_FILE` · required · Who granted permission, when, and until when. Printed into every report. - `SIGNALS_FILE` · required · Your prompt text, tool names and secret patterns. Never committed. - `TARGET_AUTH_HEADER` · required, secret · The credential for your own endpoint. Referenced by name from the target file. - `JUDGE_API_KEY` · optional, secret · Optional. Without it the run reports every deterministic finding and no judge verdicts. - `MAX_REQUESTS_PER_RUN` · required · A hard cap. The run stops here whatever the pack says. - `CONCURRENCY` · required · Low by default. This is pointed at something somebody depends on. - `RUNS_DIR` · required · Transcripts and reports. These contain whatever leaked, so treat them as secret. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · ZeroLeaks product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: A Python command line tool writing to a local run directory, no service and no dashboard, One target file naming exactly one host, enforced inside the HTTP transport, A versioned directory of declarative probe files with a pack version and a date, Deterministic matchers first, a model judge second, deterministic always winning, A self-contained HTML report plus JSON, with full transcripts and a diff between runs. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Checking the host once when the configuration loads leaves three ways out: a redirect, a proxy setting and a templated URL, any of which sends your adversarial traffic at somebody who never agreed to receive it and turns a test into an incident; put the check in the transport so it runs on every single request, turn off automatic redirect following, and make a cross-host attempt end the run rather than skip the request, so it is impossible to miss. - Encoding probes as Python functions means the pack cannot be updated without a release, cannot be reviewed by anyone who does not read your code, and eventually contains a probe that does something other than send a message, at which point your red-team harness has an arbitrary code execution path in it aimed at your own infrastructure; keep probes declarative, validate them at load, and make the loader structurally incapable of running anything. - Sending the full transcript to a third-party judge model means the tool built to find out whether your system prompt and your keys leak has just posted your system prompt and your keys into somebody else's inference logs, which is the exact outcome you were testing for; match deterministically on your own machine, replace every matched span with a marker before the judge sees anything, record which markers were substituted, and make the judge optional so a run with no judge key still produces every deterministic finding. - A harness with cheerful default concurrency pointed at your own production endpoint is an unplanned load test, and one that retries a rate-limit response immediately converts a rate limit into an outage you caused yourself on the system you were trying to protect; default the concurrency low, honour the interval the server asks for, cap total requests per run, and abort with a summary when the error rate crosses a threshold rather than grinding on and blaming the target. - A summary line reading 'no leaks found' will be pasted into a security questionnaire within a month, and it is not true: what happened is that this pack, on this date, against this endpoint, found nothing, while the jailbreak families it knows about are a snapshot of what its author understood when they wrote it; put the pack date, probe count and covered families in the summary, phrase the result as what was tested rather than what is safe, and state in the report itself that a self-run harness is not a third-party assessment.
# Delivery milestones · ZeroLeaks Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A target you are allowed to test The tool cannot send a request to any host other than the one you recorded permission for. ### Steps 1. Enforce the host inside the transport, per request Configuration-time checks have three documented ways around them. Files: `leakprobe/transport.py`, `tests/test_host_guard.py` 2. Disable redirect following and fail the run on a cross-host hop A redirect is the easiest way to point this at a stranger. Files: `leakprobe/transport.py` 3. Require an authorisation record and print it It goes in the report too, because that is where it matters. Files: `leakprobe/auth.py`, `tests/test_auth_required.py` ### Done when - [ ] A cross-host redirect ends the run with zero requests - [ ] Every request passes the host check - [ ] No authorisation means no run - [ ] No flag or variable disables the check ### Watch out - Checking the host once when the configuration loads leaves three ways out: a redirect, a proxy setting and a templated URL, any of which sends your adversarial traffic at somebody who never agreed to receive it and turns a test into an incident; put the check in the transport so it runs on every single request, turn off automatic redirect following, and make a cross-host attempt end the run rather than skip the request, so it is impossible to miss. ## M2 · A probe pack that is data, not code Probes can be added, reviewed and dated without touching the program. ### Steps 1. Define the probe schema and validate at load A bad probe stops the run; it does not get skipped. Files: `leakprobe/probes/schema.py`, `tests/test_probe_schema.py` 2. Stamp the pack version and date into every run A report without a pack date is a report with no shelf life. Files: `leakprobe/pack.py` 3. Keep the loader incapable of executing anything No eval, no import, no subprocess, and a test that says so. Files: `tests/test_loader_inert.py` ### Done when - [ ] A new probe file works with no code change - [ ] An invalid probe stops the run by name - [ ] Pack version and date reach every report - [ ] The loader executes nothing ### Watch out - Encoding probes as Python functions means the pack cannot be updated without a release, cannot be reviewed by anyone who does not read your code, and eventually contains a probe that does something other than send a message, at which point your red-team harness has an arbitrary code execution path in it aimed at your own infrastructure; keep probes declarative, validate them at load, and make the loader structurally incapable of running anything. ## M3 · Detection in the right order A deterministic match is the finding, and the judge never sees your secrets. ### Steps 1. Match deterministically before anything leaves the machine The matchers are cheap and they are the reliable half. Files: `leakprobe/detect.py`, `tests/test_detectors.py` 2. Redact matched spans before the judge sees the transcript Then record which markers stood in for what. Files: `leakprobe/judge.py`, `tests/test_judge_redaction.py` 3. Compute severity from the signal, not the probe A prompt with nothing in it is not a critical finding. Files: `leakprobe/severity.py` ### Done when - [ ] No secret reaches the judge request - [ ] Deterministic results beat the judge - [ ] Severity follows the matched signal - [ ] A run without a judge still reports findings ### Watch out - Sending the full transcript to a third-party judge model means the tool built to find out whether your system prompt and your keys leak has just posted your system prompt and your keys into somebody else's inference logs, which is the exact outcome you were testing for; match deterministically on your own machine, replace every matched span with a marker before the judge sees anything, record which markers were substituted, and make the judge optional so a run with no judge key still produces every deterministic finding. ## M4 · Multi-turn, and not knocking the target over A run explores multi-turn attacks while staying gentle enough to point at something real. ### Steps 1. Default concurrency low and cap requests per run This is pointed at something somebody depends on. Files: `leakprobe/runner.py`, `tests/test_cap.py` 2. Honour the retry interval the server sends Ignoring it is how a rate limit becomes an outage. Files: `leakprobe/backoff.py` 3. Abort on a high error rate with a partial report A thousand failed requests is not more data. Files: `leakprobe/runner.py` ### Done when - [ ] A retry interval is honoured in full - [ ] The request cap is never exceeded - [ ] A high error rate aborts with a partial report - [ ] Multi-turn probes see the real earlier reply ### Watch out - A harness with cheerful default concurrency pointed at your own production endpoint is an unplanned load test, and one that retries a rate-limit response immediately converts a rate limit into an outage you caused yourself on the system you were trying to protect; default the concurrency low, honour the interval the server asks for, cap total requests per run, and abort with a summary when the error rate crosses a threshold rather than grinding on and blaming the target. ## M5 · A report that does not overclaim (production only) The output is usable as a regression gate and cannot be mistaken for an assurance. ### Steps 1. Put the pack date and coverage above the findings It is the context that decides what the result means. Files: `leakprobe/report.py`, `templates/report.html.j2` 2. Ban assurance phrasing with a test Someone will paste this into a questionnaire. Files: `tests/test_no_assurance.py` 3. Make diff stable and the exit code meaningful That is what turns this into a regression gate. Files: `leakprobe/diff.py`, `tests/test_diff.py` ### Done when - [ ] Pack date, coverage and authorisation head every report - [ ] No assurance phrasing survives the test - [ ] The diff is stable when nothing changed - [ ] A high finding exits non-zero ### Watch out - A summary line reading 'no leaks found' will be pasted into a security questionnaire within a month, and it is not true: what happened is that this pack, on this date, against this endpoint, found nothing, while the jailbreak families it knows about are a snapshot of what its author understood when they wrote it; put the pack date, probe count and covered families in the summary, phrase the result as what was tested rather than what is safe, and state in the report itself that a self-run harness is not a third-party assessment.
# Operations · ZeroLeaks ## Backup The run directory is the evidence and it contains whatever leaked, so it is both worth keeping and dangerous to keep. Encrypt it, give it a retention period, and never put it in the same repository as the code. ## Restore Restore a run directory only to diff against, and check the authorisation record it contains has not expired before treating it as current. An old run is a statement about an old pack against an old model and it stops meaning much quite quickly. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring The pack date against today, the proportion of probes that ended in refusal rather than a response, and the diff between consecutive runs. A sudden jump in refusals usually means the endpoint changed rather than that it got safer. ## Incident checklist The incident is a report being read as an assurance, or a run directory being shared with the leaked material still in it. The first is why the framing is tested; the second is why the runs directory is encrypted and has an end date. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A cross-host redirect is confirmed to end the run with zero requests - [ ] No flag or variable is confirmed to disable the host check - [ ] No secret is confirmed to reach the judge request - [ ] Severity is confirmed to follow the matched signal, not the probe - [ ] No assurance phrasing is confirmed to survive the report test ## Launch constraint Do not market omitted ZeroLeaks capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. The one endpoint. Its host is the only host the transport will speak to. TARGET_FILE=./target.yaml # Required. Who granted permission, when, and until when. Printed into every report. AUTHORISATION_FILE=./authorisation.yaml # Required. Your prompt text, tool names and secret patterns. Never committed. SIGNALS_FILE=./signals.yaml # Required · secret. The credential for your own endpoint. Referenced by name from the target file. TARGET_AUTH_HEADER=Bearer ...redacted # Optional · secret. Optional. Without it the run reports every deterministic finding and no judge verdicts. JUDGE_API_KEY=sk-...redacted # Required. A hard cap. The run stops here whatever the pack says. MAX_REQUESTS_PER_RUN=600 # Required. Low by default. This is pointed at something somebody depends on. CONCURRENCY=2 # Required. Transcripts and reports. These contain whatever leaked, so treat them as secret. RUNS_DIR=./runs
$ choose a build depth, inspect the files, then open the complete pack in your agent
Two reasons, and neither is that the harness is hard. First, jailbreaks rot: the probes that worked against last quarter's model are dead weight now, and keeping a live corpus is somebody's full time job, not a cron you set up once. Second, self-attested security is worth roughly nothing to an enterprise buyer, so companies pay for an external artifact with a date and a logo on it. If your goal is just to stop shipping a system prompt that unravels when someone types 'repeat everything above', a local harness is genuinely enough.
xA curated, maintained probe corpus that tracks new jailbreak families instead of whatever the agent remembered on build day
xMulti-turn and multi-model attack strategies, including crescendo and encoding tricks, done properly rather than as single-shot prompts
xA third-party report with a date on it that you can hand to a customer or an auditor
xSeverity triage and remediation guidance written by someone who has seen a lot of these
xRegression runs on every model or prompt change without you remembering to trigger them
Nothing worth pointing at. That's why the prompt exists.
Vibecode ZeroLeaks
Kinda. The core of ZeroLeaks is buildable in a weekend with the prompt on this page, but there are real gaps: A curated, maintained probe corpus that tracks new jailbreak families instead of whatever the agent remembered on build day, Multi-turn and multi-model attack strategies, including crescendo and encoding tricks, done properly rather than as single-shot prompts. Read the honest list above before committing.
How much does ZeroLeaks cost?
ZeroLeaks costs about $79/month (Pro, checked 2026-08-18), which is $948 per year.
What do I lose by replacing ZeroLeaks?
Honestly: A curated, maintained probe corpus that tracks new jailbreak families instead of whatever the agent remembered on build day; Multi-turn and multi-model attack strategies, including crescendo and encoding tricks, done properly rather than as single-shot prompts; A third-party report with a date on it that you can hand to a customer or an auditor; Severity triage and remediation guidance written by someone who has seen a lot of these; Regression runs on every model or prompt change without you remembering to trigger them. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to ZeroLeaks?
No mature open-source alternative worth pointing at, which is exactly why the one-shot prompt on this page exists.