The verdict that arrived after the turn ended
An async hook cannot block and its output waits for the next turn. Three ork verifiers run async and still compute a real block verdict, which is the #3779 class: a finding established after the moment it could act. CC's asyncRewake lets a background hook that exits 2 wake Claude at once. Pick a mode and read the timeline of a measured run.
1. Where the finding lands
Measured on CC 2.1.251 and again on 2.1.248 with a PostToolUse hook that sleeps 300 ms, prints a nonce to stderr and exits 2, driving a real claude -p against a scripted loopback Messages API (two Reads, then text; zero model spend). Under asyncRewake the nonce is in request 3 as <system-reminder> Stop hook blocking error from command "PostToolUse:Read": <nonce> .... Under plain async the same hook ran (marker file written twice) and the nonce never reached any request in the session.
2. What changed in ork
| piece | change | proof |
|---|---|---|
bin/run-hook.mjs | --rewake flag: after the usual JSON, a continue: false result is repeated on stderr as [<hook>] <reason> and the process exits 2; everything else exits 0 as before | run-hook-rewake.test.ts: blocks exit 2 with the reason, non-blocks exit 0, and without the flag a block still exits 0 |
hooks.json | "asyncRewake": true + --rewake on posttool/write/stale-import-detector, skill/coverage-threshold-gate, skill/cross-instance-test-validator: the three async entries whose code path returns a block | structure, schema and count gates green; 175 entries unchanged |
| floor | none needed | the field is honored on 2.1.248 (below today's floor) and plugin validate passes it on 2.1.248, 2.1.250, 2.1.251; validate also passes a made-up field name, so only the run counts |
3. The one that did not fire, and why that is its own finding
A second run drove the built plugin through the same stub with a project where three files import ./util/b and CC rewrote src/util/b.ts. stale-import-detector ran (hook-timing: 2 ms, ok) and returned silent success, so nothing was there to rewake. Called directly on the same fixture it also returns {"continue":true,"suppressOutput":true}. The wiring is correct; the detector's own grep path appears not to find what is in front of it. That is reported separately rather than folded into this change, because a rewake cannot deliver a verdict the verifier never establishes.