propguard
v0.1.45v0.1.44 integrates this line with the remote's: the session-scoped skip now carries a third state. Session-scoping fixed WHOSE token is consumed; it did not fix WHEN. A Stop is adjudicated by four independent gate processes, so a token honoured by one that allows was burned even when another blocked the same stop — the stop never happened and the one-shot escape had been spent on nothing, which is standing pressure toward a permanent bypass (CLAUDE.md 5). `consume_session_skip` now takes `stop_hook_active` and parks an honoured token as `<id>.skip.honoured`: it is re-honoured while a re-entry after a block is in flight, and deleted the first time a chain actually ends. A rename failure that can neither bound nor clear the token is resolved to NOT honouring it. v0.1.41: the shared project-root `.propguard-skip` marker is gone. The operator escape is now `propguard skip --reason "..."`, which is scoped to the issuing session (CLAUDE.md 5 forbids a shared one-shot file under parallel sessions, since whichever session stops next consumes it), requires a non-empty reason, and appends both the issue and the consumption to the gate log so a bypass cannot happen unrecorded. v0.1.38: closes the fail-open v0.1.37 documented but deliberately left open (backlog 87dbfbb8 p0 + d8e22b26 p1). `run_git` now answers `Determination<String>` and `diff_text` answers `Determination<DiffText>`, forwarding (never re-minting) the boundary's `Undetermined` from any of the four reads that feed it: `git diff`, `git diff --cached`, the untracked `ls-files`, and an untracked file's body. `evaluate` maps that to a new `decide_diff_failed` — tag `diff-read-failed`, bounded by max_attempts, escapable, no hash recorded, no per-property violations attributed — placed BEFORE the `match cfg.mode` split so both inject and subprocess modes route through it. Measured before the fix with the real `git`: a tracked file whose working-tree content is not valid UTF-8 (no NUL, so git emits a textual diff) gave changed_files=Files(["bad.rs"]) with diff_text="" and truncated:false, and the gate returned ALLOW tag=empty-diff; the valid-UTF-8 control gave BLOCK tag=below-threshold. The partial case is closed the same way and deliberately returns no diff at all rather than the readable half: previously `git diff` succeeding while `git diff --cached` could not be decoded produced a diff mentioning a.rs, omitting b.rs, and carrying truncated:false — announcing itself complete over propguard's only incompleteness signal. `empty-diff` survives but now means only what it says: every read succeeded and the diff really was empty. New tests pin both faults end-to-end through `evaluate`, each with an anti-vacuity control that differs only in whether the bytes decode, plus a genuinely-clean-repo allow and a measured demonstration that Mode::Subprocess really can reach properties-satisfied (so the guard prevents a reachable allow, not a hypothetical one). v0.1.37: docs-only correction of a claim v0.1.36 itself introduced. `run_git`'s comment said dropping `None` matched a benign 'fail gracefully / treat as git-unavailable' convention because 'callers already tolerate None/empty output'. That is false on the decision path: `changed_files` recovers (None -> collect false -> ChangeScan::Failed -> block), but `run_diff` and the untracked `ls-files` read inside `diff_text` still drop `None` silently, and a tracked file whose working-tree content is not valid UTF-8 yields changed_files = Files(["bad.rs"]) with diff_text = "" and truncated:false, which gate.rs's `if diff.trim().is_empty() { allow("empty-diff") }` ALLOWS (the valid-UTF-8 control gives a 131-byte diff and a BLOCK). The comment now states which caller is hardened and which two are not, and names the open backlog ids 87dbfbb8 and d8e22b26. The behaviour is deliberately NOT fixed here. v0.1.36: an unreadable git stdout no longer reads as a clean repo — harness-core 0.2.3's bounded pipe read stopped discarding the read error and stopped folding an expired read budget into an empty string, so a `git` that exits 0 while its output never arrives now reaches ChangeScan::Failed (gate fails closed) instead of the Files(vec![]) a genuinely clean repo produces. v0.1.32: fixed a stale never-break-a-turn docstring/comment in main.rs and README.md — panics no longer swallow-to-exit-0; the Stop hook's panic path resolves via harness_core::gate::run::run_guarded's fail-closed policy (block on the first stop, bounded allow only on a second consecutive stop_hook_active panic). No behavior change. v0.1.16: merged two independently-developed Continuous-Audit fix lines that landed on the same v0.1.14 base — (a) stopped correlation-store pollution (CA-propguard-03/04): a checker-unavailable or diff-truncated Block evaluated no property, so it no longer stuffs the full derived prop_ids into the property_id-keyed fleet-correlation signal as if they were real per-property violations (it reports an empty set, mirroring the below-threshold narrowing); (b) documented the checker subprocess timeout (checker_timeout_secs, default 300s, process-tree kill on timeout) in README.md and added config-propagation tests covering the propguard.toml override path. Earlier v0.1.13 fixed 3 subprocess-hang gaps (CA-propguard-004/005/006) — checker timeout now kills the whole shell-spawned process tree via a Unix process group instead of just the direct shell child; stdout is read on a bounded thread so a lingering process holding the pipe open can't hang past the timeout; and all git subprocess calls now go through a single timeout-bounded choke point that fails gracefully instead of hanging the Stop hook indefinitely. Property gate for Claude Code: on Stop, derive 3–5 semantic properties (invariants) from a task's done_criteria and check the generated code against them before the agent can declare done. In inject mode it blocks and injects a property checklist for the running agent to self-verify (no API key); in subprocess mode it runs an independent checker and counts per-property PASS/FAIL, blocking when fewer than a threshold hold. Goes beyond 'concrete tests pass'. Subscription-native, bundled Rust binary.
By yukineko0 GitHub starsUpdated yesterday
Directory evidence
- Runtimes
- Claude Code
- Parsed components
- 0 skill or MCP entries
- Source updated
- Sep 23, 2026
- Manifest status
- Canonical path parsed
The directory validates manifest shape and source location. It does not execute the plugin or provide a security endorsement. Review the indexing methodology →
Install propguard for Claude Code
claude plugin marketplace add IchenDEV/agent-plugin-mkt
claude plugin marketplace update agent-plugin-marketplace
claude plugin install propguard@agent-plugin-marketplacePaste and run these commands in a terminal with Claude Code. They add and refresh the PluginsMP catalog, then install this plugin.
The installer fetches third-party code from the source repository shown on this page. This directory validates manifest structure and source location, but does not perform a security audit; review the manifest, components, and source before installing.
Get the source manually
git clone https://github.com/yukineko/claude-harnessesClone the source repository, then follow its setup instructions to add the plugin to a compatible client. The plugin root is crates/propguard/.
Plugin files
└── .claude-plugin/plugin.json
Plugin manifests1
{
"name": "propguard",
"version": "0.1.45",
"description": "v0.1.44 integrates this line with the remote's: the session-scoped skip now carries a third state. Session-scoping fixed WHOSE token is consumed; it did not fix WHEN. A Stop is adjudicated by four independent gate processes, so a token honoured by one that allows was burned even when another blocked the same stop — the stop never happened and the one-shot escape had been spent on nothing, which is standing pressure toward a permanent bypass (CLAUDE.md 5). `consume_session_skip` now takes `stop_hook_active` and parks an honoured token as `<id>.skip.honoured`: it is re-honoured while a re-entry after a block is in flight, and deleted the first time a chain actually ends. A rename failure that can neither bound nor clear the token is resolved to NOT honouring it. v0.1.41: the shared project-root `.propguard-skip` marker is gone. The operator escape is now `propguard skip --reason \"...\"`, which is scoped to the issuing session (CLAUDE.md 5 forbids a shared one-shot file under parallel sessions, since whichever session stops next consumes it), requires a non-empty reason, and appends both the issue and the consumption to the gate log so a bypass cannot happen unrecorded. v0.1.38: closes the fail-open v0.1.37 documented but deliberately left open (backlog 87dbfbb8 p0 + d8e22b26 p1). `run_git` now answers `Determination<String>` and `diff_text` answers `Determination<DiffText>`, forwarding (never re-minting) the boundary's `Undetermined` from any of the four reads that feed it: `git diff`, `git diff --cached`, the untracked `ls-files`, and an untracked file's body. `evaluate` maps that to a new `decide_diff_failed` — tag `diff-read-failed`, bounded by max_attempts, escapable, no hash recorded, no per-property violations attributed — placed BEFORE the `match cfg.mode` split so both inject and subprocess modes route through it. Measured before the fix with the real `git`: a tracked file whose working-tree content is not valid UTF-8 (no NUL, so git emits a textual diff) gave changed_files=Files([\"bad.rs\"]) with diff_text=\"\" and truncated:false, and the gate returned ALLOW tag=empty-diff; the valid-UTF-8 control gave BLOCK tag=below-threshold. The partial case is closed the same way and deliberately returns no diff at all rather than the readable half: previously `git diff` succeeding while `git diff --cached` could not be decoded produced a diff mentioning a.rs, omitting b.rs, and carrying truncated:false — announcing itself complete over propguard's only incompleteness signal. `empty-diff` survives but now means only what it says: every read succeeded and the diff really was empty. New tests pin both faults end-to-end through `evaluate`, each with an anti-vacuity control that differs only in whether the bytes decode, plus a genuinely-clean-repo allow and a measured demonstration that Mode::Subprocess really can reach properties-satisfied (so the guard prevents a reachable allow, not a hypothetical one). v0.1.37: docs-only correction of a claim v0.1.36 itself introduced. `run_git`'s comment said dropping `None` matched a benign 'fail gracefully / treat as git-unavailable' convention because 'callers already tolerate None/empty output'. That is false on the decision path: `changed_files` recovers (None -> collect false -> ChangeScan::Failed -> block), but `run_diff` and the untracked `ls-files` read inside `diff_text` still drop `None` silently, and a tracked file whose working-tree content is not valid UTF-8 yields changed_files = Files([\"bad.rs\"]) with diff_text = \"\" and truncated:false, which gate.rs's `if diff.trim().is_empty() { allow(\"empty-diff\") }` ALLOWS (the valid-UTF-8 control gives a 131-byte diff and a BLOCK). The comment now states which caller is hardened and which two are not, and names the open backlog ids 87dbfbb8 and d8e22b26. The behaviour is deliberately NOT fixed here. v0.1.36: an unreadable git stdout no longer reads as a clean repo — harness-core 0.2.3's bounded pipe read stopped discarding the read error and stopped folding an expired read budget into an empty string, so a `git` that exits 0 while its output never arrives now reaches ChangeScan::Failed (gate fails closed) instead of the Files(vec![]) a genuinely clean repo produces. v0.1.32: fixed a stale never-break-a-turn docstring/comment in main.rs and README.md — panics no longer swallow-to-exit-0; the Stop hook's panic path resolves via harness_core::gate::run::run_guarded's fail-closed policy (block on the first stop, bounded allow only on a second consecutive stop_hook_active panic). No behavior change. v0.1.16: merged two independently-developed Continuous-Audit fix lines that landed on the same v0.1.14 base — (a) stopped correlation-store pollution (CA-propguard-03/04): a checker-unavailable or diff-truncated Block evaluated no property, so it no longer stuffs the full derived prop_ids into the property_id-keyed fleet-correlation signal as if they were real per-property violations (it reports an empty set, mirroring the below-threshold narrowing); (b) documented the checker subprocess timeout (checker_timeout_secs, default 300s, process-tree kill on timeout) in README.md and added config-propagation tests covering the propguard.toml override path. Earlier v0.1.13 fixed 3 subprocess-hang gaps (CA-propguard-004/005/006) — checker timeout now kills the whole shell-spawned process tree via a Unix process group instead of just the direct shell child; stdout is read on a bounded thread so a lingering process holding the pipe open can't hang past the timeout; and all git subprocess calls now go through a single timeout-bounded choke point that fails gracefully instead of hanging the Stop hook indefinitely. Property gate for Claude Code: on Stop, derive 3–5 semantic properties (invariants) from a task's done_criteria and check the generated code against them before the agent can declare done. In inject mode it blocks and injects a property checklist for the running agent to self-verify (no API key); in subprocess mode it runs an independent checker and counts per-property PASS/FAIL, blocking when fewer than a threshold hold. Goes beyond 'concrete tests pass'. Subscription-native, bundled Rust binary.",
"author": {
"name": "yukineko"
},
"keywords": [
"hooks",
"property-based",
"invariants",
"quality",
"gate",
"rust"
]
}For maintainers
If you maintain this plugin, link to this source-backed listing from your README so users can review its manifest and indexed components.
[propguard on Agent Plugins Marketplace](https://pluginsmp.com/plugins/propguard)