Migrating from v0.6.10 to v0.6.11
v0.6.11 turns Patchwork from an audit trail into an audit trail + safety layer. The audit chain is unchanged; the new piece is a taint-aware PreToolUse enforcement layer that can DENY or APPROVAL-REQUIRE some tool actions your agent previously got to take.
This guide answers three questions:
- What will start failing that didn't fail before?
- What knobs do I have when it does fail?
- How do I roll back if I need to?
What will start failing
If your agent's workflow includes any of these patterns, expect denials in v0.6.11:
Reading a README / docs / changelog and then immediately running a network fetch. README contents register
prompttaint (FORCE_UNTRUSTED_PATTERNSalways wins), so a subsequentcurl,gh gist create,git pushto a new remote, etc. trips the dangerous- shell-combos classifier. This is the design — README-says-run-this is the canonical prompt-injection vector.curl … | shorbash <(curl …)patterns. Always denied under any taint. Considered "always dangerous." If you legitimately need this, usepatchwork approve <id>to authorize a one-shot retry.npm install(orpnpm/yarn/bun) of an untrusted package after any taint. Lifecycle scripts run as your user. Use--ignore-scriptsif you're certain, orpatchwork approveto authorize.Writing to persistence paths after taint.
.github/workflows/,~/.ssh/,.git/hooks/, shell rc files, etc. The persistence sink's severity flips fromapproval_requiredtodenyunder taint.Reading any in-repo path that isn't on a
trusted_pathsallowlist. Until you mark code subtrees explicitly trusted viapatchwork trust-repo-config "src/**", every Read raisesprompttaint. (For Claude this typically means: tainted forever after the first Read. Once trusted_paths is configured for code paths, Reads of source files don't taint.)
The three escape valves
patchwork approve <request_id>
When Patchwork denies an action with permissionDecision: "deny", the denial reason includes a request_id and tells the user (not the agent) to run the approve command in their own terminal. Run it in your own shell — not inside the agent's session.
# Agent sees:
# [Patchwork] denied: ... (rule: sink_deny)
# Ask the human user to run `patchwork approve 7d3c81c773959f7f` in their own terminal.
# You run:
patchwork approve 7d3c81c773959f7f
# Agent retries the same action — token is single-use, TTL 5min default.patchwork approve (no arg) lists all currently-pending requests.
Why the agent can't approve for you. v0.6.11 layers three defenses against the agent self-approving: a system-policy regex deny that refuses the Bash invocation before it spawns, a semantic detector that walks the parsed shell tree and matches
patchwork/admin-verb argv shapes regardless of quoting, and an in-CLI TTY check. The remaining same-UID residuals (symlink/alternate-name exec, variable-named exec) are documented in threat-model.md § Accepted residuals; they are structurally fixed by the v0.6.12 root-owned approval daemon, which moves the boundary out of the agent's UID entirely.
patchwork clear-taint
If the session's taint accumulated from sources you trust (a curl that fetched docs from a known-safe domain, an MCP response you've audited), declassify and continue:
patchwork clear-taint # clear all non-secret kinds
patchwork clear-taint prompt # clear just one kind
patchwork clear-taint secret --allow-secret # secret requires explicit opt-in
patchwork clear-taint --session ses_abc # specific sessionCleared sources are NOT deleted from the snapshot — they're tombstoned with a cleared field for the audit trail. Future re-reads will see the declassification.
patchwork trust-repo-config
Mark in-repo subtrees as trusted so reads inside them don't raise prompt taint:
patchwork trust-repo-config "src/**" # add a glob
patchwork trust-repo-config --list # see current trusted_paths
patchwork trust-repo-config --remove "src/**" # take one back outThis writes a project-local .patchwork/policy.yml overlay. The system policy at /Library/Patchwork/policy.yml still controls every deny rule — trusted_paths is the one knob a project policy can additively express to narrow taint posture (it cannot weaken enforcement).
FORCE_UNTRUSTED_PATTERNS always win: README, CHANGELOG, docs/, examples/, .changeset/, node_modules/, vendor/, dist/, build/ — none can be silenced by trusted_paths.
Rolling back
If v0.6.11's enforcement is too aggressive for your workflow today and you need the v0.6.10 behavior:
# Install the previous version globally
npm install -g patchwork-audit@0.6.10
# Re-run the installer to refresh the Claude Code hooks
patchwork initYour existing audit data is forward + backward compatible — JSONL events, SQLite events, DSSE attestations, and seal chains are all stable across this boundary.
What stays the same
~/.patchwork/events.jsonlhash-chained audit log/Library/Patchwork/events.relay.jsonlroot-owned audit log- HMAC-SHA256 seals every 15 min
- DSSE / in-toto v1 commit attestations
patchwork log,patchwork export,patchwork verify,patchwork doctor,patchwork commit-attest— all unchanged- The system policy at
/Library/Patchwork/policy.ymlstill controls rule-based deny
Required upgrade step: system-policy refresh
v0.6.11 ships a new deny rule in docs/default-policy.yml that refuses agent invocations of patchwork approve | clear-taint | trust-repo-config at the PreToolUse layer (before the Bash subprocess spawns). For existing installs this rule needs to land in your live system policy at /Library/Patchwork/policy.yml.
This is a manual edit. patchwork init installs hooks; it does not refresh an existing system policy, and there is no --upgrade flag (earlier revisions of this guide referenced one that was never implemented).
If you used the system-level install, the policy file is flagged immutable and even root cannot write it until you clear the flag. That is the hardening working as intended, but it does mean the edit is a three-step operation. Check first:
ls -lO /Library/Patchwork/policy.yml
# -rw-r--r-- 1 root wheel schg ... <- schg means immutableOn macOS, clear the flag, edit, then put it back. Leaving it cleared silently downgrades your install:
sudo cp /Library/Patchwork/policy.yml "/Library/Patchwork/policy.yml.bak-$(date +%Y%m%d)"
sudo chflags noschg /Library/Patchwork/policy.yml
# ... make the edit below ...
sudo chflags schg /Library/Patchwork/policy.yml
ls -lO /Library/Patchwork/policy.yml # confirm schg is backOn Linux the equivalent is sudo chattr -i then sudo chattr +i; on Windows, attrib -R then attrib +R.
Add this rule under commands.deny: in /Library/Patchwork/policy.yml:
- regex: "(^|[^A-Za-z0-9_-])patchwork[ \\t]+(approve|clear-taint|trust-repo-config)\\b"
action: deny
reason: "Administrative CLI — must be run by the human user in their own terminal"Known limitation: this rule matches command text, not parsed commands
The rule is a regular expression evaluated against the raw command string, so it has no way to tell an actual invocation from the phrase merely appearing in the text. It will therefore also deny commands such as:
- writing documentation or code comments that mention
patchwork approve git commit -m "..."whose message describes the admin CLIgreporrgsearching your codebase for those verb names- test fixtures containing them
This fails closed, which is the safe direction, and the in-process check (isAdminCliInvocation) is argv-aware and does not have this problem. But it is a real nuisance, most of all if you work on Patchwork itself.
Until it is narrowed to require command position, the workaround is to avoid the literal phrase in a command — for example build it from parts in a script file and run the script by name, rather than typing it on the command line. Tracked for a fix that evaluates policy against parsed argv rather than text.
Confirm it took effect with patchwork policy show | grep clear-taint, and confirm the immutable flag is restored with ls -lO.
Note that patchwork policy validate reports on the file you point it at, so a failed write followed by a successful validate is not evidence the edit landed. Grep the active policy instead.
You do not need to restart or redeploy the relay daemon for a policy change: the hook loads the policy fresh on each invocation. Avoid an unnecessary restart, because any event in flight when the relay socket goes away is dropped.
Until this rule is in place, the in-CLI TTY check is the only gate against agent self-approval, and a PTY-allocating wrapper (script -c '...') will pass it. If you installed v0.6.11 by following an earlier revision of this guide, assume the rule is missing and check.
What's new for v0.6.11 (TL;DR)
- New CLIs:
patchwork approve,patchwork clear-taint,patchwork trust-repo-config. - New per-session taint state at
~/.patchwork/taint/<sha256>.json. - New approval request files at
~/.patchwork/approvals/<id>.pending.json<id>.approved.json.
- New PolicySchema field:
trusted_paths: string[]. - New user-level trust store at
~/.patchwork/trusted-repos.yml(replaces the project-local.patchwork/policy.ymltrust overlay that a hostile repo could commit — see R2-003). - New relay-config field:
socket_group: string(fixes the silent EACCES regression from v0.6.10). - New system-policy regex deny for admin CLIs (R3-001 / R4-001 / R6-001 hardening across six adversarial audit rounds).
- New semantic
admin_cli_invocationsink in dangerous-shell-combos (R5-001 / R6-001) — quote- and modifier-aware. - Broadened env-source detector covers
env,printenv, bareset,export -p,declare -p/-x/-px/-xp,typeset -p/-x/-px,readonly -p,compgen -e, and any redirect or argv reference to/proc/<X>/environ(R2-004 / R3-003 / R4-002). - Tests: 943 → 1509. Build clean across all packages.
Where to look when something denies
- Read the denial reason in the agent's tool-use error. It names the rule (
policy_deny,bash_unknown_indicator_taint,sink_deny,sink_approval_required,default_allow) and surfaces thepatchwork approvecommand. ~/.patchwork/events.jsonlrecords the denial with full context.patchwork statusshows the current session's taint summary.patchwork doctorvalidates the whole stack (hooks, relay, policy, seals).
See docs/v0.6.11/threat-model.md for the full picture of what v0.6.11 defends against and what it doesn't — including the "Same-UID approval boundary" section that explains why some bypass shapes are accepted residuals in v0.6.11 and structurally closed by the v0.6.12 root-owned approval daemon.
What's coming in v0.6.12
The v0.6.11 audit loop spent six rounds hardening the agent-self-approval boundary at the layer where it lives today (CLI + system-policy regex). Each round closed concrete bypasses, but they're all variations of one root cause: approval is a command that the agent's own UID can type. v0.6.12 fixes this structurally:
- A small
patchwork-approverdaemon will run as root, owning the approval state in a root-only directory. The agent's UID cannot read or write that state. - The
patchwork approveCLI as the agent knows it will go away. Approval will surface through a channel the agent doesn't share (desktop notification / Touch ID / phone push / web UI on localhost — configurable). - The PreToolUse hook will ask the daemon over a Unix socket "is request
<x>approved?" Yes/no. The agent cannot forge the daemon's response.
That removes the same-UID boundary from the approval flow entirely, making the R6-002 alternate-name and variable-named-exec residuals moot (there's no command to call, regardless of what the agent names its binary). The same daemon will sign per-session taint snapshots with a root-held HMAC key, closing the snapshot authenticity residual (R1-001 / R1-008) at the same time.
v0.6.12 will also ship the first-class URL allowlist + body-shape detection that closes allowed-domain exfiltration.