Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion TODO.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Todo

- preallow: generate the credential rules from [Betterleaks](https://github.com/betterleaks/betterleaks) instead of gitleaks, which now takes only security fixes, once Betterleaks 2 is released. Its `sourcegraph-access-token` rule no longer matches any 40-character hex string, so outputs holding both a git commit hash and the word `sourcegraph` stop being withheld. Most of its rules filter a match with an Expr expression and some score it by tokens (`tokenRatio`), so `scripts/gitleaks.ts` has to turn those into JavaScript or leave such rules out, and the keyword budget test still has to pass.
- preallow: generate the credential rules from [Betterleaks](https://github.com/betterleaks/betterleaks) instead of gitleaks, which now takes only security fixes, once Betterleaks 2 is released. Most of its rules filter a match with an Expr expression and some score it by tokens (`tokenRatio`), so `scripts/gitleaks.ts` has to turn those into JavaScript or leave such rules out, and the keyword budget test still has to pass.
2 changes: 1 addition & 1 deletion plugins/preallow/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "preallow",
"version": "0.0.9",
"version": "0.0.10",
"description": "Lets Claude Code auto mode run the steps of the task you gave without the classifier: a model judge allows what is reversible or what you explicitly asked for",
"author": {
"name": "Entwining"
Expand Down
4 changes: 2 additions & 2 deletions plugins/preallow/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ The README states what the plugin allows and what it leaves to native approval;

The plugin only loosens. `tool.check` turns the core's `ask` into `allow` or returns the core's answer unchanged; it never touches an `allow`, a `deny`, or an ask whose result names a decider other than the classifier (`OWNS_ASK` in `hooks/register.ts`), or whose input names a host as `approver`. Preserve this ownership boundary when the engine adds metadata. An unknown field leaves the ask to the judge, since sending every ask with an unknown field to the classifier would bring back the false denials the plugin exists to remove. A permission hook that throws or overruns its budget is skipped and the core's answer stands.

One judge for every tool it is asked about: no per-tool or per-command policy branches or allowlists in its policy or around it, and the policy text names no project, command, or wording taken from real data. A tool whose verdict the engine keeps whatever a hook answers (`RESERVED_TOOLS` in `hooks/register.ts`) never reaches the judge, since a judge call there spends seconds on an allow the engine rejects or ignores; add a tool there only for that engine behavior, as a `rejected` log line or the engine's documentation shows it. A Bash, PowerShell, or command-monitor call requesting a non-empty `allowed_domains` list also stays with the classifier, whose approval registers the network grants. Nor does a call whose input asks to run in the background, since its output arrives where the withholding scan cannot see it; a tool whose output arrives later without such a flag (`Monitor`) is a known gap in the README until the engine signals it.
One judge for every tool it is asked about: no per-tool or per-command policy branches or allowlists in its policy or around it, and the policy text names no project, command, or wording taken from real data. A tool whose verdict the engine keeps whatever a hook answers (`RESERVED_TOOLS` in `hooks/register.ts`) never reaches the judge, since a judge call there spends seconds on an allow the engine rejects or ignores; add a tool there only for that engine behavior, as a `rejected` log line or the engine's documentation shows it. A Bash, PowerShell, or command-monitor call requesting a non-empty `allowed_domains` list also stays with the classifier, whose approval registers the network grants. Nor does a call whose input asks to run in the background, since its output arrives where the output scan cannot see it; a tool whose output arrives later without such a flag (`Monitor`) is a known gap in the README until the engine signals it.

`JUDGE_SYSTEM` (`hooks/policy.ts`) decides what is allowed. Check a change to it or to either stage's default model or effort in `hooks/register.ts` against real denials kept outside this repository, on a split not used while writing the change, and compare allow and review rates before and after.

Expand All @@ -18,6 +18,6 @@ Only a reply that ends with a JSON object answering `allow` allows: text may com

`judge` (`hooks/judge.ts`) stays one stage, since the evaluation outside the engine calls it once per stage; the second stage lives in `judgeCall` (`hooks/stages.ts`). The `model`, `effort`, `escalation_model`, and `escalation_effort` settings reach `$.model.complete` as the user wrote them: no aliases, option lists, or validation.

A failed output scan withholds the output of a call the plugin allowed; the call has already run, so recovery must not execute it again. Every pattern in `hooks/withhold.ts` stays linear in the output's length, and `scripts/gitleaks.ts` strips the name gitleaks matches before a value under a keyword, which backtracks for seconds over a crafted megabyte, so a scan ends within the budget instead of withholding large outputs; the test that scans every rule's keyword fails when it comes back. A pattern needs a key's body, not only the words that name its format, and `scripts/gitleaks.ts` appends a token body to a gitleaks rule that is only a literal prefix, since the plugin's own files name every format; `bun run test:sources` fails when reading one of them would be withheld.
The output scan answers a call the plugin allowed with a new `{ result }` holding the redacted record and no `ref`, since core reuses the messages a `ref` names and the model would read the original, or, after an error, with a deny holding the redacted error text. Core keeps whole the context a hook below attached, so a credential there withholds the output. A redacted call stays in `allowedCalls` for the rest of the session, because the engine refuses an answer only after the hook has returned it, then skips the hook and lets the unredacted result stand unless `.catch` withholds it; `claude plugin test` does not validate a hook's result against the tool's schema, so no test reaches that refusal. A failed scan withholds the output too; the call has already run, so recovery must not execute it again. Every pattern in `hooks/withhold.ts` stays linear in the output's length, and `scripts/gitleaks.ts` strips the name gitleaks matches before a value under a keyword, which backtracks for seconds over a crafted megabyte, so a scan ends within the budget instead of withholding large outputs; the test that scans every rule's keyword fails when it comes back. A pattern needs a key's body, not only the words that name its format, and `scripts/gitleaks.ts` appends a token body to a gitleaks rule that is only a literal prefix, since the plugin's own files name every format; `bun run test:sources` fails when reading one of them would be redacted.

`hooks/gitleaks.generated.ts` is written by `bun run gitleaks` (`scripts/gitleaks.ts`, which pins the gitleaks release and rewrites its Go regexes for JavaScript); change the script and regenerate instead of editing the file.
9 changes: 8 additions & 1 deletion plugins/preallow/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
# Changelog

## 0.0.9 - unreleased
## 0.0.10 - 2026-10-29

- An output holding something credential-shaped has each such value replaced with `[redacted by preallow: <kind>]`, wherever it appears in the result, instead of being withheld whole, so the agent keeps the rest of it; a note beside the result tells it what was replaced. The whole output is still withheld when the full output a large result leaves in a file holds a credential, when a note another hook adds beside the result holds one, or when the text the agent reads shows one the tool's result does not. A credential the scan does not know now reaches the agent even beside one it found. The log records `redacted` with the kinds it replaced.
- The output scan also reads the tool's result, not only the text the agent reads, so a credential that line numbers or JSON escaping hide from its patterns, such as a password in a netrc file the agent reads with Read, is found.
- An empty assignment such as `API_KEY=` is no longer taken for a credential whose value is the name on the next line, as in an `.env.example` file.
- A git commit hash in an output that names Sourcegraph is no longer taken for a Sourcegraph access token; a token without the `sgp_` prefix is still found under a name such as `SRC_ACCESS_TOKEN`.

## 0.0.9 - 2026-10-29

- The judge runs on `haiku` at `high` effort instead of `opus` at `medium`, and a second judge on `opus` at `medium` judges again, from the start, every call the first did not allow, including one it gave no verdict on; the second judge's verdict is final. The new `escalation_model` and `escalation_effort` settings choose the second judge; if you set `model` and leave `escalation_model` empty, there is none. Each `judged` line in the log keeps the first judge's fields and gives the second judge's in `escalation`.
- The judge's instructions are rewritten as decisions taken in order: leaks first, then whether the call serves your task and respects what you ruled out, then work on your own repositories, then whether each effect can be undone, and last whether your words explicitly cover the irreversible ones. They describe each field the judge reads and each fact a check returns, including what a fact does not establish, such as file times saying nothing about who made a file. The wording was chosen by evaluating it against real calls.
Expand Down
Loading
Loading