v0.4.0
0.4.0 — Witnessed enforcement on Cursor and Codex
MINOR: plugin packaging output changes (spec fixes and three new formats), and the
vendored PreToolUse adapter changes -- the deny-dialect and payload-decoding fixes below
mean an adopter's next chock sync rewrites it. Every other compiled enforcement
surface is unchanged.
-
Per-vendor marketplace indexing:
chock marketplace build --tree cursor|codex
indexes a vendor's own format tree with the index file its client actually reads --
Cursor's.cursor-plugin/marketplace.jsonin Cursor's schema, and for Codex the
legacy.claude-plugin/marketplace.jsonshape it was witnessed consuming from git
marketplaces -- so each vendor-named distribution repo carries exactly one vendor's
packages instead of every format tree. Default (--tree claude) is byte-unchanged. -
cursorandcodexplugin formats:chock plugin build --format cursor|codex
packages a policy for Cursor and OpenAI Codex, with the enforcing hook each vendor
actually reads. Same guard, same adapter, byte-identical to every other format --
only the envelope differs.--format allnow emits five trees.- Cursor (
.cursor-plugin/plugin.json+ flathooks/hooks.json) subscribes to
beforeShellExecution, the shell-scoped eventchock syncalready installs, so a
plugin install and a repo install run the identical hook. Nomatcheris emitted:
under this event the matcher is a regex over the COMMAND TEXT, not a tool name, so
the other formats'"Bash"would match almost nothing and silently disable the
guard. Cursor ignores Agent Plugins hooks entirely, so this is the only format that
enforces there. - Codex (
.codex-plugin/plugin.json+ nestedhooks/hooks.json) subscribes to
PreToolUsewith matcherBash-- Claude's protocol exactly. The legacy
.codex-plugin/manifest is deliberate: Codex's loader DISCARDS hooks from an
Agent-Plugins-format manifest (codex-rs/core-plugins/src/loader.rs), so shipping
the Copilot package there would install a plugin whose enforcement is deleted at
load time while its description still claimed it.
- Cursor (
-
Codex packages claim witnessed enforcement -- via JSON deny, not exit codes.
Probed on a real Codex Desktop install (Windows 11): a trusted PreToolUse hook
returning the documented exit-2 deny ran the command three times, because Codex wraps
Windows hook commands inpowershell -Command, which collapses exit 2 into 1 -- and
Codex's parser treats that as a failed hook and FAILS OPEN (its only stdout-parsing
arm is exit 0). With the deny carried inhookSpecificOutput.permissionDecisionJSON
on a clean exit, the same command was witnessed BLOCKED (2026-08-24). The adapter now
speaks that dialect to Codex-shaped payloads (turn_idpresent); Claude Code keeps
its witnessed exit-2 path. Conditions stated in the package posture: Codex hooks are
UNTRUSTED on install until a human approves a per-hook trust review, that trust is
bound to a hash of the hook command so a plugin update silently voids it until
re-approved, and every hook failure fails open. The emitted hooks.json also carries no
top-leveldescription: Codex < 0.143.0 rejects the whole file over that one key and
silently drops every hook in it (openai/codex#30397). -
Cursor packages DO claim enforcement, witnessed blocking on a real install after the
two fixes below, with a benign command in the same session still allowed. -
Two silent fail-opens fixed, both found by probing a real Cursor install (neither
is visible in any documentation, and each would have shipped a package that advertised
enforcement and delivered none):- Payload decoding. Cursor prefixes its hook payload with a UTF-8 BOM, and
sys.stdin.read()decoded those bytes with the platform locale (cp1252 on Windows),
turning the BOM into three stray characters.json.loadsthen failed, the adapter
reported "not checked", and returned 0 -- every command ALLOWED. The payload is now
read as bytes and decodedutf-8-sig, which also stops non-ASCII paths being mangled. - Deny signalling. Cursor documents exit 2 as "equivalent to returning
permission: deny". For plugin hooks that is false: a hook returning exit 2 with the
reason on stderr was witnessed NOT blocking -- the command ran. The adapter now also
emits Cursor's stdout response ({"permission": "deny", "user_message", "agent_message"}) for Cursor-shaped payloads only; Claude and Copilot still get
exit 2 with an empty stdout, asserted by test.
Witnessed blocking on a real Cursor install after both fixes, with a benign command in
the same session still allowed.
- Payload decoding. Cursor prefixes its hook payload with a UTF-8 BOM, and
-
A silent guard can no longer become a silent allow: the PreToolUse adapter now
guarantees a reason on stderr for every deny. Codex records exit 2 with an empty
stderr as a FAILED hook ("did not write a blocking reason to stderr",
codex-rs/hooks/src/events/pre_tool_use.rs) and lets the command run, so a guard that
denied without explaining itself would have enforced nothing there while every other
client showed a deny. Harmless elsewhere; load-bearing on Codex. -
The marketplace lockfile test now derives its expected tree set from
FORMATSrather
than an enumerated list -- the same under-coverage a hand-written list caused once
before, where a newly added tree escaped the check while it still read as complete. -
Bundled authoring skills use the same flat metadata: the five shipped skills
(chock-init, eval, optimize, policy-init, validate) carried the nestedchock:
object the packaged-policy fix removed — two metadata dialects in one project.
Their frontmatter is now the same flat string map (lists comma-joined, booleans
as "true"/"false"), and manifest ingestion decodes the typed fields; the derived
manifests are proven identical. Old nested frontmatter still loads, so
third-party skills are unaffected. -
SKILL.md
metadataspec fix: the Agent Skills spec (which Agent Plugins 1.0
defers to for SKILL.md) requiresmetadatato map string keys to string values;
the packaged skills nested achock:object there, which awesome-copilot'svally
linter rejected ("Metadata values must be strings"). Now a flat map with dotted
keys (chock.artifact,chock.enforcement,chock.coverage_without_chock) —
same facts, spec-conformant shape. A parsed-not-substring test pins the constraint. -
Posture-aware skill frontmatter: a hook-carrying package's SKILL.md used to
statecoverage_without_chock: advisorynext to the very hook that enforces —
one package stating and refuting a claim at once. The frontmatter now swaps the
advisory claim for the shipped hook's path (chock.hooks), the same substitution
the hook emitters already made in plugin.json and the closing note. -
copilotplugin format:chock plugin build --format copilotemits the Agent
Plugins 1.0 layout — rootplugin.json,skills/— with the enforcing PreToolUse
hook undercom.github.copilot/hooks/hooks.json, the namespace directory VS Code
documents for Agent Plugins hook bundles and non-Copilot clients must ignore. This
is the shape spec-validating marketplaces (awesome-copilot) accept; the Claude
layout, which Copilot also reads, keeps its manifest in.claude-plugin/and fails
their intake. Hook command, adapter and guard are byte-identical to the Claude
package's (asserted in tests): two formats, one enforcement system. The posture is
scoped to this format's audience — generic Agent Plugins clients are required to
ignorecom.github.copilot, so the description names where the hook enforces
(documented for VS Code agent mode) and that a namespace-ignoring client gets the
advisory skill only. Hook-carrying packages replace thecoverage_without_chock
extension claim with the hook's location, and the danglingmanifest: manifest.yaml
pointer (a file this out-of-place format never ships) is dropped — each package
carries only claims that are true of it.--format allnow emits three trees.