Repository navigation
feat: wire the pre-write gate on Codex and stop four silent allows - #105
Merged
Merged
Conversation
Codex CLI had no pre-write gate: hooks-codex.json wired only SessionStart and
UserPromptSubmit, so a TaskFlow run there had no gate, no evidence capture, and
one reminder per prompt. runtime.md recorded that as a limitation of the host.
It was not - Codex's own hook docs (read 2026-10-29) document PreToolUse firing
on apply_patch, permissionDecision: deny, and additionalContext. CONTRIBUTING
now states the distinction, because "not wired" and "not supported" are
different facts and only one of them is work.
capability-gate learned the shape Codex sends: it reads the patch text under
tool_input.command, walks the four directives that name a target, and judges
each path with the rule the file_path shape already used. A move is Update
File: <old> plus Move to: <new>, so both ends are judged - reading only the
destination leaves the departure unchecked. A patch naming several files is
judged path by path and a refusal ends the run, so a patch is all-or-nothing
as the host applies it. A non-empty patch with no recognizable directive is
refused, because a gate that cannot read its host must not report success.
Review found four ways that gate allowed writes it exists to refuse, none of
them in the fixtures:
- set -- $patch_paths expanded unquoted, so a path containing a space split
into two paths that were each judged and each allowed.
- a CRLF patch - routine on Windows, the host this wiring is for - left a
trailing \r on every path, so the basename matched no stage and the gate
returned 0.
- releases were written as each path was judged, so a refused two-path patch
still spent an evidence line behind a write the host never applied.
- consumed was recomputed per path, so two releases in one patch both claimed
sequence 1.
The last two are one defect - deciding and writing were interleaved - and are
fixed by queuing releases and writing them once, after every path is judged.
Each has a fixture, and each fixture reddens when its fix is reverted.
Also in this change, both from the same audit:
- capability-gate never read ## Approval. It now refuses a write whose
recorded approval names a different Task version, and allows every other
write: approval is the permit to start work, so refusing unapproved writes
would deadlock every new task at its first step. The decision lives in a
new hooks/approval-check shared with completion-gate, whose verdicts on all
93 archived Plans are unchanged, message included.
- completion-gate read the last - Status: line of a Step carrying two. It now
refuses and names the Step: the two lines state different statuses, and
which is intended is not a question a file reader can answer.
A per-Step evidence requirement was measured and rejected rather than shipped:
it was satisfied by three throwaway "ok" lines on a Plan where nothing was
done, and refused 73 of 93 archived Plans. Recorded in the task's reject-a16.md.
The Codex column is documented, not measured - no real Codex has run this. Its
real-machine check must include a *** Move to: case.
hkwuks
marked this pull request as ready for review
October 30, 2026 14:28
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Codex CLI had no pre-write gate.
hooks/hooks-codex.jsonwired onlySessionStartand
UserPromptSubmit, so on that host a TaskFlow run had no gate, no evidencecapture, and one reminder line per prompt — the flow was guidance there, not a
lifecycle.
skills/taskflow/references/runtime.mdrecorded this as a limitation ofthe host. It was not: Codex's own hooks documentation documents
PreToolUsefiringon
apply_patch,hookSpecificOutput.permissionDecision: "deny", andadditionalContext. The distinction between not wired and not supported wasthe defect, and
CONTRIBUTING.mdnow states it as a rule.This PR wires the gate, teaches it the input shape Codex sends, and closes two
holes in the existing gates that the same audit turned up.
TaskFlow traceability
TaskFlowDocs/2026-10-29-midturn-presence/hooks/hooks-codex.json— newPreToolUseonapply_patch→capability-gate.hooks/capability-gate— reads a patch as well as a path; refuses a write whoserecorded approval names a different Task version.
hooks/approval-check(new) — the one implementation of the approval decision,shared by
completion-gateandcapability-gate.hooks/completion-gate— refuses a Step carrying two- Status:lines.hooks/smoke-test— replaces the assertion that forbade wiring Codex with onethat runs the wiring, plus the fixtures for each new condition.
hooks/README.md,skills/taskflow/references/runtime.md,CHANGELOG.md,CONTRIBUTING.md— the host-coverage rule, the Codex column and section, theinventory entries, the changelog.
TaskFlowDocs/todo.md— the promoted entry's status.TaskFlowDocs/todo.mdentryTF-20261030-15b439, an unrelateduser request recorded during this task. It is a Todo entry only — no code.
mainhttps://github.com/hkwuks/TaskFlow— read withgit remote get-url origin; one remote, no fork, no credentials in this body.Verification
Hooksworkflow — CI is the authority perCONTRIBUTING.md; this branch ispushed and the run is read there rather than reproduced locally
git diff --check— cleanquick_validate.py skills/taskflow:Skill is valid!## Verification / Review, Steps 1–6Measured locally before the push, and recorded in the Plan:
bash hooks/smoke-test—ALL SMOKE PASSED.hooks/completion-gategives the same verdict and the same message on all 93archived Plans before and after the approval decision moved out: 19 refused, 74
passed, 0 disagreements. The refusal text every caller matches on is unchanged.
hooks/capability-gate'sfile_pathpath was compared against its previous selfover 11 event shapes and matched byte for byte — the Codex work is additive.
diff -q), eachreddening its own named assertion. Two mutations found real gaps rather than
confirming the code: the
movefixture passed with theMove to:arm deleted,because the
Update File:line named the gated document too — amove-destination-onlycase was added; and a review pass after the suite wasalready green found four ways the patch gate allowed writes it exists to
refuse, listed below.
(1)
set -- $patch_pathsexpanded unquoted, so a path containing a space splitinto two paths that were each judged and each allowed; (2) a CRLF patch — routine
on Windows, the host this wiring is for — left a trailing
\ron every path, sothe basename matched no stage and the gate returned 0; (3) releases were written
as each path was judged, so a refused two-path patch still spent an evidence line
behind a write the host never applied; (4)
consumedwas recomputed per path, sotwo releases in one patch both claimed sequence 1. (3) and (4) are one defect —
deciding and writing were interleaved — and are fixed by queuing releases and
writing them once, after every path has been judged. Each has a fixture, and each
fixture was verified to redden when its fix is reverted. This is the reason the
Codex path needs a real-machine run before its column says
measured: none ofthese shapes was in the original fixtures, and a real host may carry more.
Review boundaries
addition outside the task, and it is a record, not a change to any behaviour
Known limitations and follow-ups
runtime.mdcolumn saysdocumented, not yet measuredand stays that way until asession on a real machine confirms the deny reaches the agent — including one
*** Move to:case, since the destination's spelling is the part the docs leaveleast clear. Recorded as A14 in the Plan.
PostToolUseis deliberately not wired.capability-evidence's predicate is acapability class (
Skill,Agent|Task,mcp__*) andapply_patchis an edittool; wired, the hook would run, exit 0, and record nothing while
runtime.mdclaimed evidence capture. Measured: feeding it that event changed no evidence line.
evidence requirement (
donerequires a## Verification / Reviewline naming thatStep) was implemented far enough to test: it was satisfied by three throwaway
oklines on a Plan where nothing was done, and it refused 73 of 93 archived Plans —
including 7 of the 24 that really did use
hooks/task progress. It measureswhether verification was documented per Step, not whether it happened, so it was
dropped. The evidence is in the task's
reject-a16.mdandevidence-a16-fake-plan.md, and A16 in the PRD records the reversal.not wired, notnot supported: its file tools registeras lowercase
write/edit, which no matcher arm matches. Left for its own task.