zernie c4e75ae538 fix: read a space-separated tool list, so a working fence stops auditing as ineffective (#221)
* fix: read a space-separated tool list, so a working fence stops auditing as ineffective

Claude Code documents three equivalent spellings of `allowed-tools:` /
`disallowed-tools:` / a subagent's `tools:` — a comma-separated string, a
SPACE-separated string, and a YAML list. `splitList` in core/frontmatter-read.ts
split on `,` alone, so the space-separated form arrived as ONE token matching no
built-in name.

Reported with a reproduction by @vlad-ryzhkov in #217: on a 22-skill harness,
changing only the separator moved Safety 81 -> 80 -> 81, reversibly. Reproduced
here before changing anything — three of the seven documented shapes came back as
a single token: `WebFetch WebSearch`, `Bash(git add *) Bash(git commit *)`, and
`"Read Write Glob"` (a YAML-quoted scalar whose VALUE is a space-separated list,
which is what most skills in the wild write).

TWO SURFACES, AND THE SECOND IS WORSE THAN THE REPORTED ONE. #217 asks about a
skill's fence, where `allowed-tools:` only pre-approves, so a mis-split MISLEADS:
a fence Claude Code really enforces was reported as closing no lethal-trifecta
leg, naming as still-supplied the very tools the author had just denied. But a
SUBAGENT's `tools:` is the allowlist the PreToolUse rail denies against, and it
reads the same `splitList`. Measured on `tools: Read Grep Glob`, pre-fix:

  Read DENY · Grep DENY · Bash DENY

— the joined token matched nothing, so the agent was denied EVERY tool, including
the three it was explicitly granted. #217 raised this path as an open question
("I have not worked out whether anything downstream depends on that"); it did.

THE NAIVE FIX IS WORSE THAN THE BUG. `split(/[,\s]+/)` shreds `Bash(git push *)`
into `Bash(git`, `push`, `*)`, and `bashGrantIsUnbounded()` answers "unbounded"
when it cannot recognise a grant — so a BOUNDED grant would begin reading as an
unbounded one. That trades a false-clean verdict for a false-exposed one, which
is the worse side of the trade core/lethal-trifecta.ts already reasons about, for
a reason the author cannot see anywhere in their file.

So this is a tokenizer: whitespace separates only at paren depth 0. Quotes are
stripped per token AFTER splitting, never treated as a delimiter — the outer
quotes of `"Read Write Glob"` are YAML syntax, and treating them as token
boundaries would keep exactly this bug for every skill that quotes its list. An
unclosed `(` keeps the remainder as one token rather than fragmenting it into
garbage tool names.

The tokenizer is the one @vlad-ryzhkov proposed, with his six-row table plus
irregular spacing and an unbalanced paren as tests. Every new test is
mutation-proven with the patch verified landed:

  separator back to `,` only (pre-fix)          -> the 4 new tests fail
  whitespace splits at ANY depth (the naive fix) -> the parens test fails

One home, so the surfaces cannot drift: `frontmatterList` is the single reader
behind scan, adopt, and the rail.

Closes #217

Co-Authored-By: vlad-ryzhkov <211849729+vlad-ryzhkov@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: a parenthesis inside a quoted string is a character, not a bracket

Follow-up within #217, found by the Codex review bot on the first cut of the
tokenizer. The depth counter read every `(` as structure, so a grant carrying a
literal unmatched paren in its own command left depth at 1 after the grant
closed and swallowed everything after it:

  tools: Bash(printf '( %s' foo) Read
    ->  ["Bash(printf '( %s' foo) Read"]     one token

On a subagent that is the same failure this PR exists to fix: the rail denies
`Read`, a tool the author granted. Reproduced before changing anything.

Quote state now gates the DEPTH COUNTER ONLY, never the splitting. That
asymmetry is the fix, not an accident: if being inside quotes also suppressed
whitespace splitting, a quoted list would collapse back to one token and
reintroduce #217 itself for every skill that writes `allowed-tools: "Read Write
Glob"`. Backslash escapes are honoured outside single quotes, per shell rules.

MUTATION-PROVEN BOTH WAYS, each with the patch verified landed:

  depth counted inside quotes (the pre-fix state)  -> the new test fails
  quotes suppress splitting too (the over-fix)     -> the new test fails

🔴 AND THE SECOND MUTATION EXPOSED A DEFECT IN THE TEST FIRST. Its original
counterweight asserted the quoted list on a VALID YAML block and stayed GREEN
under the widening mutation — because js-yaml strips the quotes before
`splitList` ever runs, so no quote character was present and the mutation could
not bite. Measured, then moved onto a MALFORMED block, where the regex salvage
path hands the quotes through: that is the only path on which the property is
observable at all. A green mutation is a finding about the test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: vlad-ryzhkov <211849729+vlad-ryzhkov@users.noreply.github.com>
2026-09-09 21:37:34 +05:00

vigiles logo

vigiles

You have a rule your agent follows half the time — and no way to know which one.

Verify your CLAUDE.md or AGENTS.md, skills, and hooks are real — and prove they actually work.

▶ Grade any public repo live at vigiles.sh — no install, runs in your browser.

npm version CI License live demo


You review every PR. Nothing reviews your CLAUDE.md.

Your harness — the CLAUDE.md or AGENTS.md rules, skills, subagents, and hooks steering your agent — is the one part nobody checks. Nobody verified it's real. Nobody tested it works. That's not a system. That's vibes.

And vibes break silently mid-task: a subagent wired to a tool that doesn't exist, two skills your agent can't tell apart, one helper quietly able to read your secrets and send them out.

vigiles1 checks your harness is real, not just well-formed — Claude Code and Codex alike. One command, no key, no config, safe on any repo:

npx vigiles audit

It's free and open-source, runs entirely on your machine, and never bills per token. (eval is the only step that calls a model — on your own Claude subscription.) Here's what it caught on plugins people actually ship. ↓

What it caught

vigiles audit report for my-plugin: a verdict header reading 'Two one-line fixes away from a B.' next to a C (77/100) grade, a five-category strip (Truthfulness, Triggering, Structure, Safety, Tested), ranked fix cards with '+N pts' impact badges, broken-reference findings — and, lower down and badged experimental, a 'Your rules → enforced' preview mapping a prose rule the config silently turns off

Like Google's Lighthouse, but for your agent harness. One command grades it A–F across six categories, leads with a plain-English verdict — "two one-line fixes away from a B" — and ranks every fix by the points it buys back:

  • Truthfulness — do the references resolve?
  • Triggering — do skills fire, without colliding?
  • Structure — are tool contracts and configs valid?
  • Safety — any way for the agent to leak your data?
  • Tested — does the harness ship deterministic tests?
  • Evaluated — has anything measured whether your skills actually fire? (Distinct from 0: if nothing asked, it says not measured.)

And it closes the loop from prose to enforcement: your rules → enforced maps each rule you wrote to the lint rule that actually enforces it — already on, one config line away, or silently turned off (below).

These are real scans of public plugins — run npx vigiles audit <any-repo> for your own. The examples below use Claude Code subagents; the same checks run on Codex AGENTS.md, skills, and hooks. ↓

Every one of these is valid markdown — parses fine, does the wrong thing. That's the gap a style linter can't see:

  • A tool your agent thinks it has and doesn't — a subagent lists a tool that isn't available to it. The harness drops it silently; the agent loses a capability it believes it has.
  • Two skills your agent can't tell apart — near-identical descriptions, so the agent (which picks by reading them) fires the wrong one. One popular plugin ships 45 such pairs.
  • A rule you wrote that nothing enforces — "always use ===" while eqeqeq is set to "off" in your ESLint config. Your CLAUDE.md says enforce it; your config quietly turns it off.
  • A subagent that can read your secrets and send them out — one holding all three "lethal-trifecta" legs (reads data · takes untrusted web input · can send data out). A poisoned page makes it POST your .env anywhere — no exploit, just the tools it was given, hidden behind a healthy-looking grade.

▶ See these live at vigiles.sh — grade any repo · everything it catches →. Point audit at a whole marketplace and it ranks every plugin the same way.

How it works — vibes → verified

audit shows you where your setup is still vibes. Turning that into verified is four commands over one engine — and almost none of it needs a model or a key.

Command Answers Needs a model? When to run
audit Everything, graded A–F No — read-only2 Anytime; it's the report
lint Do the structural checks pass? No CI gate, every push
test Does the harness behave? No — a scripted stand-in Every commit
eval Does a skill actually help? Yes — your subscription On demand

One engine, two doors. audit is the local report; lint is the CI gate that fails the build on the same deterministic checks — broken refs, bad tool contracts, dead hooks, skill collisions (Proofs 1–2). test and eval go further: past does it exist to does it work. (init / compile / eject manage the optional typed-spec layer for the structural rules no linter can express — a graduation step you rarely run by hand. If you use a spec, run compile in CI too: it is what re-derives that spec's refs, while lint verifies the compiled file is intact.) How the verbs relate →

🔎 Lint — your instructions stop lying

Every path, script, symbol, and rule verified against reality — plus tool contracts, skill collisions, and dead hooks (the catches above). You don't write the checks, and you don't port anything into a spec: lint reads the CLAUDE.md or AGENTS.md you already hand-edit and checks it as-is. Rules that map to a real linter rule run in your own config (ESLint, Ruff), not a shadow layer. How →

🧪 Test — does the harness actually do its job?

A hook that blocks nothing, a skill that hijacks unrelated prompts, context that never reaches the model — each passes a naive "did it run?" check. That gap is false confidence: a guard that looks like it works and silently doesn't. vigiles tests the real thing — hooks block, skills fire, subagents finish what they promised, a stray git push is caught before it happens. It drives a scripted stand-in for the model, not a live call, so it needs no key and runs on every commit. How testing works → · Got a safety hook? Prove it blocks rm -rf and force-push →

Nothing you scan leaves your machine. lint, audit and the deterministic test tiers make no network call at all — no telemetry, no analytics, no HTTP client in the package. Evals drive your own claude CLI on your own subscription, so no third party is introduced. What is and isn't transmitted →

📊 Eval — the only way to put a real number on cost

"Caveman Mode cuts 65% of your tokens." Says who? vigiles A/Bs the claim on real coding tasks and hands you three numbers: the token bill, whether it hit its target, and whether your code still works.

caveman vs baseline · sonnet · 7 tasks × 5 trials · $0 on your subscription
  output tokens   6% lower on average   (the claim was 65% — and it GREW on 2 of 7 tasks)
  bill            $0.5396 → $0.5334     (flat — output is only ~20% of the cost)
  correctness     1.0 → 1.0             (nothing broke)

Point it at any harness change that claims a number — does a compression skill pay for itself, is a subagent worth its cost, which model is cheapest here. promptfoo and DeepEval bill per token, every run; vigiles runs on your own Claude Pro/Max subscription, so you measure on every change, not once. A committed lock file (like package-lock) keeps CI honest without re-calling the model. (Claude Code today; Codex landing.) Measure a skill →

Quick start

1. See what's broken — read-only, no setup (or try it in your browser first at vigiles.sh):

npx vigiles audit

2. Set it up when you like what you see. Paste into Claude Code or Codex:

Set up vigiles in this repo: run `npx vigiles init` and accept the defaults. If I
already have a CLAUDE.md or AGENTS.md, audit it and show me which references are
stale and which of my rules aren't enforced. Then write + run one harness test for
a hook or skill of mine. Don't run a real-model eval without asking me first.

Or run it yourself:

npx vigiles init   # sets up the typed spec for structural rules (non-destructive — eject reverses), adds CI,
                   # installs vigiles's skills + hooks as a Claude Code plugin. The plugin
                   # CONTENT goes to ~/.claude/, never your repo; init also commits a two-key
                   # reference to it in .claude/settings.json so teammates get prompted to
                   # install it rather than silently missing it. Codex: skills install globally.

Already have a harness, or a non-JS repo? npx vigiles init --ci-only sets up just the CI integrity gate — nothing installed, zero conflict. When to use gate vs full →

Interactive in a terminal, non-interactive for agents/CI (or --yes). Works with Claude Code and Codex — vigiles verifies CLAUDE.md and AGENTS.md the same way. Codex setup →

Adoption is smooth: one command, then your agent does the rest. init installs the skills and hooks, so a plain-English ask does the work — no specs to hand-write, no hooks to wire:

  • "test my skills" → scaffolds and runs a trigger/behaviour test, then commits its result so CI can check it (test-harness)
  • "harden my rules" → upgrades prose guidance into enforced linter rules (strengthen)
  • "add a rule to my CLAUDE.md or AGENTS.md" → edits the source and recompiles (edit-spec)

The hooks keep it honest in-loop — nudging the agent to tag a linter-rule mention so vigiles can verify it, or to re-run a test whose result just went stale — so there are no chores to remember.

What init sets up
  • Both lint and test by default; scope with --lint / --test.
  • Already have a CLAUDE.md / AGENTS.md, skills, or subagents? audit and lint read them as-is — nothing is moved or rewritten. For the structural rules that want a typed spec, init sets one up non-destructively (eject undoes it).
  • Adds vigiles to devDependencies; installs the Claude Code plugin (skills + hooks) via the marketplace — globally, never vendored.
  • Declares that plugin in your repo's .claude/settings.json (extraKnownMarketplaces + enabledPlugins, merged into whatever is already there). This is a reference, not content — nothing is vendored, and it does not install the plugin for a teammate: an external-source plugin declared project-level doesn't load until each person installs it. What it buys is that Claude Code prompts them with the install command, instead of a fresh clone silently having the npm package and none of its skills.
  • Wires CI as a zernie/vigiles@v1 workflow (needs only read + PR-comment permissions) that posts a sticky PR comment + a valid output.

Targets Claude Code and Codex out of the box, or your own harness. Prefer to write tests yourself? JS or TS (*.harness.{mjs,ts}) — run with npx vigiles test.

FAQ

  • Is this a framework I have to build around? No. It's a tool you run — like ESLint, Lighthouse, or npm audit. One command, a report, an optional CI gate. There's a library API for automation, but you never touch it to get value.
  • Isn't this just a markdown linter? No — it checks whether your instruction file is true (every path/script/symbol/rule exists and is enabled), then tests and measures your harness. A style linter can't do any of that.
  • Does anything leave my machine? No. audit and lint read your local repo — no upload, no account, no server. (eval is the only step that calls a model, on your own Claude subscription; the browser demo only reads a public repo you name via GitHub's API.)
  • What do I have to change to adopt it? Almost nothing. Plain markdown works with zero new files, rules run in your existing linter, and the agent edits the specs for you. The typed spec is opt-in, only for structural checks a linter can't express — like TS's strict (why?).
  • Non-JS repo? npx vigiles lint verifies your CLAUDE.md or AGENTS.md with no install (Ruff/Clippy/golangci-lint/detekt/… too — 11 linters across Python, Rust, Go, Kotlin, Java, Ruby, CSS).

Full FAQ →

Not for you if you want a model/capability benchmark or runtime guardrails in the request path — vigiles is build-/CI-time.

Docs

vigiles.sh is the live demo — grade any repo in your browser. The docs index is the full map, grouped by what you're doing:

A name starting with experimental_ is not covered by semver. It may change shape or disappear in a patch release. Everything so named has a stable alternative, given in that feature's own docs page — and the prefix is the only signal you need to look for, since it is on every call site rather than on an import line you scrolled past. See Stability.

Project — Stability · Contributing · Related tools · ships as an Agent Plugins 1.0.0 plugin (how to do the same)

License

MIT


  1. vigiles — the watchmen of ancient Rome, who guarded the city (and fought its fires) by night. Quis custodiet ipsos custodes? — "who watches the watchmen?" (Juvenal, Satire VI). ↩︎

  2. audit reads only by default. Two deeper checks — live MCP connections and skill-firing — are opt-in and ask before they run. ↩︎

S
Description
edit-spec: Edit a vigiles .spec.ts to change a compiled instruction file (CLAUDE.md / AGENTS.md) — add, modify, or remove a rule, section, command, or key file. Use…; strengthen: Upgrade a vigiles spec's guidance() rules to enforce() — scan the guidance rules in a CLAUDE.md/AGENTS.md spec and find existing linter rules (ESLint, Ruff,…; test-harness: Install vigiles and test a Claude Code harness — hooks, skills, agents, settings, CLAUDE.md — by picking the right tier (unit / deterministic / eva…
Readme MIT 24 MiB
Languages
TypeScript 92.1%
JavaScript 6.7%
Shell 0.8%
HTML 0.2%
CSS 0.1%