Skip to content

Codex backend bricked by fix-required dispatch gate — reclassify skill_load_guard codex_status and add real-registry dispatch contract test #4082

Description

@Trecek

Failure

Every run_skill dispatch on the codex backend crashes unconditionally since commit bfc5d2c70 (PR #4070, closes #4001, shipped v0.10.760+):

RuntimeError: Cannot dispatch skill 'make-plan' on backend 'codex': HOOK_REGISTRY contains fix-required entries [Read|Write|Edit|Bash|Grep|Glob] that cannot be enforced by this backend.

Observed in production run impl-20260611-163753-197057 (2026-06-11 16:37 PDT, installed v0.10.763, token-reserve project). Same-day regression: the identical order dispatched make-plan successfully on v0.10.751. The only bypass is a step-level non-Anthropic provider profile carrying ANTHROPIC_BASE_URL (reroutes to the claude-code backend object, tools_execution.py:744-758).

Root cause mechanics

_check_backend_compat_get_fix_required_hook_matchers (src/autoskillit/server/tools/tools_execution.py:90-164) computes frozenset({"skill_load_guard"}).issubset(frozenset({"write_guard"}))FalseSkillResult.crashed() for every codex run_skill. Inputs:

  • HookDef(matcher=r"Read|Write|Edit|Bash|Grep|Glob", scripts=["guards/skill_load_guard.py"], session_scope="headless_only", codex_status="fix-required", mechanism="deny")src/autoskillit/hook_registry.py:297-303
  • CodexBackend.capabilities.applicable_guards = frozenset({"write_guard"})src/autoskillit/execution/backends/codex.py:500

Why "fix-required" is wrong and the gate protects nothing on codex

  1. The runtime guard already self-bypasses on codex: skill_load_guard.py:103-104 exits 0 when its stem is absent from AUTOSKILLIT_APPLICABLE_GUARDS (codex sets write_guard). The session the gate blocks would run identically with the guard present-but-inert.
  2. Codex has no Skill tool — the guard's entire premise (deny native tools until Skill loads SKILL.md context, PR Hook-Based Skill Loading Guard for Non-Anthropic Providers + Directive SKILL.md Descriptions #1937, commit 3eec9faee 2026-05-05) is structurally inapplicable. The release-flag writer skill_load_post_hook is already codex_status="not-applicable" (hook_registry.py:290-296), so the guard could never release even if registered.
  3. The matcher names Claude tools (Read/Bash/Grep); codex tool names are read_file/shell/grep.
  4. Upstream Codex CLI cannot enforce it at any config level: read_file/grep tool handlers never emit PreToolUse hook events ([Feature request] Extend PreToolUse hooks beyond Bash + implement updatedInput rewrite openai/codex#18491, Inconsistent PreToolUse hook coverage across tool handlers (most tools never emit hook events) openai/codex#20204 — both OPEN as of 2026-06-11, no ETA; official docs call hooks "a guardrail rather than a complete enforcement boundary"). The waiver is permanent, not temporary — "fix-required" can never transition to "works-as-is" via upstream fixes currently in flight.

Changes

  1. Reclassify: src/autoskillit/hook_registry.py:301 — change codex_status="fix-required" to codex_status="not-applicable", with a registry comment citing the evidence above (no Skill tool on codex; release flag unwritable; Claude-specific matcher; upstream read-path hook events do not exist). The Literal type at hook_registry.py:30 already admits "not-applicable". _SKIP_CODEX_STATUSES (execution/backends/_codex_hooks.py:24) treats both statuses identically, so codex config.toml generation is unchanged. Two co-required documentation edits in the same file:
    • Update the Codex Compatibility Table row at hook_registry.py:82 from skill_load_guard | fix-required to | not-applicable (the table is the human-readable status reference; leaving it stale contradicts the code).
    • Extend the status taxonomy comment at hook_registry.py:49 — the current definition ("Hook targets a tool/event that Codex does not support") only partially fits, since codex has a Bash-equivalent (shell); add a clause noting not-applicable also covers hooks whose required hook events are permanently non-firing on the backend. Alternative: introduce an explicit "unenforceable-waived" literal if maintainers prefer to preserve the distinction "backend has the failure mode but no enforcement exists" — either resolution is acceptable, but the chosen one must update the taxonomy comment, and a permanently-unsatisfiable fix-required feeding a crash gate must not remain.
  2. Sync the registry hash (REQUIRED — without it CI fails): _canonical_registry_payload serializes codex_status for every hook (hook_registry.py:413-438, field at line 420), so this change alters HOOK_REGISTRY_HASH. test_committed_registry_hash_matches_live_registry (tests/hooks/test_hook_executability.py:81-98) asserts the committed src/autoskillit/hooks/registry.sha256 matches the live hash. After editing the registry, run task sync-hooks-hash (Taskfile.yml:276) and commit the updated registry.sha256.
  3. Real-registry dispatch contract test (the test that would have caught this at the PR): in tests/arch/test_capability_consistency.py, add a test class parametrized over real BACKEND_REGISTRY (no monkeypatching), asserting _get_fix_required_hook_matchers(backend.capabilities.applicable_guards) == [] for every backend not listed in a new explicit, documented DISPATCH_INCAPABLE_BACKENDS: frozenset[str] allowlist (test-local module constant; empty after the reclassification). Pure module-level constants, xdist-safe. These changes MUST land together: with the old status the test fails on codex; with the new status the allowlist stays empty. Note: post-fix the assertion holds vacuously (zero fix-required entries remain) — that is the point: the test's value is as a regression guard asserting "every registered backend must have zero uncovered fix-required hooks," and the allowlist exists to force future exceptions to be explicit rather than silent.
  4. Optional hardening: (a) add _EXPECTED_NOT_APPLICABLE and _EXPECTED_FIX_REQUIRED (expected: empty set) snapshot-set tests alongside _EXPECTED_DEGRADED (tests/hooks/test_hook_registry.py:380-392) so future status reclassifications are deliberate, not silent — this also compensates for test_generate_codex_hooks_config_excludes_fix_required (tests/hooks/test_hook_registry_codex_status.py:30-40) becoming vacuous once the fix-required set is empty; (b) refresh the stale "fix-required hook dispatch gate" phrase in the tools_execution.py size-budget description at tests/arch/test_subpackage_isolation.py:978-979.

Test impact (validated, then adversarially re-validated)

  • WILL FAIL without Change 2: test_committed_registry_hash_matches_live_registry (tests/hooks/test_hook_executability.py:81-98) — run task sync-hooks-hash.
  • TestHookFixRequiredDispatchGate (tests/server/test_run_skill_backend_compat.py:197-401): all five tests monkeypatch HOOK_REGISTRY — unaffected.
  • test_generate_codex_hooks_config_excludes_fix_required / _excludes_not_applicable (tests/hooks/test_hook_registry_codex_status.py): read the registry dynamically — still pass (the former becomes vacuous; see Change 4a).
  • test_pretooluse_deny_mechanism_is_set: exempts not-applicable entries — still passes, but silently stops checking this entry's mechanism field (accepted minor coverage loss; Change 4a's snapshot tests compensate).
  • test_capabilities_applicable_guards (tests/execution/backends/test_codex_backend.py:134-135) and test_commands_skill_session.py:75: assert applicable_guards fields, which this change does not touch — unaffected.
  • Claude-code and non-Anthropic-provider protection unchanged: CLAUDE_CODE_CAPABILITIES.applicable_guards (core/types/_type_backend.py:220) untouched; the guard still enforces where declared.

Known limitation (document, do not fix here)

_get_fix_required_hook_matchers consults the codex-specific codex_status field for whatever backend is being dispatched (tools_execution.py:90-100 takes only applicable_guards). For any future third backend the gate would evaluate CODEX statuses — a category error that currently passes only by accident. The contract test in Change 3 inherits this shape. The long-term fix (per-backend status maps or a renamed dispatch_gate_status) is out of scope here but should be recorded as a known limitation in the registry comment.

Process notes

Acceptance criteria

  1. hook_registry.py skill_load_guard entry no longer has codex_status="fix-required"; the registry comment documents the permanent-unenforceability evidence with upstream issue links; the compatibility table row (line 82) and the status taxonomy comment (line ~49) are updated consistently with the chosen status.
  2. task sync-hooks-hash has been run and the updated src/autoskillit/hooks/registry.sha256 is committed; test_committed_registry_hash_matches_live_registry passes.
  3. A non-monkeypatched contract test iterates real BACKEND_REGISTRY × real HOOK_REGISTRY and fails if any backend outside DISPATCH_INCAPABLE_BACKENDS has uncovered fix-required hooks; DISPATCH_INCAPABLE_BACKENDS is empty.
  4. run_skill dispatch on the codex backend passes _check_backend_compat (verifiable with a real CodexBackend instance and the real registry).
  5. _EXPECTED_NOT_APPLICABLE and _EXPECTED_FIX_REQUIRED snapshot tests added (or explicitly declined with rationale).
  6. Codex config.toml hook generation output is byte-identical before/after (status remains in _SKIP_CODEX_STATUSES).

Parallel-safety note for triage

Implementable in parallel with the companion open_kitchen preflight issue #4083 — zero file overlap (this issue: hook_registry.py, hooks/registry.sha256, tests/arch/, tests/hooks/; companion: server/tools/tools_kitchen.py, tests/server/).

Investigation

Prior investigation completed interactively (deep mode: code-path trace, git/design-intent timeline, test-gap analysis, adversarial challenge round, external research on Codex CLI hook capabilities, 2 post-report validators), followed by a 2-agent adversarial validation round on this issue body whose evidence was independently re-verified against the codebase (registry-hash impact, comment-table row, #3800/#4018/#4040 interactions). Full report at .autoskillit/temp/investigate/investigation_codex_fix_required_dispatch_gate_2026-06-11_171934.md in the development checkout.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugExisting behavior is brokenrecipe:remediationRoute: investigate/decompose before implementationstagedImplementation staged and waiting for promotion to main

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions