fix(zcode): strip unknown keys from the hooks block — strict schema rejects the whole config otherwise - #555
Conversation
61a5776 to
edab260
Compare
|
Please add a real CLI end-to-end validation report showing the behavior after this fix. The current description covers reproducing the issue and inspecting the reconciled config, but it would help to confirm that the user-facing workflow actually recovers:
Please add the steps that actually passed and their results to Validation / Test Plan so reviewers can verify the complete flow from teamai syncing the config to ZCode executing the hook. |
|
@jeff-r2026 Thanks for the detailed checklist — here is the end-to-end validation, all steps actually executed on Windows (10.0.22631, Node v22.22.1, CLI built from this branch, ZCode desktop 3.9.2 / CLI 0.16.5). 1. Poisoned config + commands run (isolated HOME):
2. What changed after inject (existing valid hooks preserved):
3. Real-machine migration incident (the incident that motivated this PR):
All of the above is now folded into the PR description (Validation section) with the environment and commands, per your checklist. The PR description also documents the two mechanism-level findings from the same dogfooding: the win32 |
5d863dc to
89aeb04
Compare
ZCode validates the hooks block against a strict schema and rejects the entire block on any unrecognized key (config_file_invalid → hookCount 0 → no hook fires, with the runner still reporting installed). A single hand-added annotation key therefore silently kills every hook. reconcileZcodeFormat now heals the config by keeping only the keys the schema knows (enabled/events), so a poisoned config recovers on the next inject/pull instead of staying dead.
Session-start dispatches carry a network pull to the team host; on slower links that exceeds the 10-15s builtin shell-hook defaults, so ZCode killed the hook mid-pull (observed: hook.run.failed at durationMs 10023 with a 10000ms timeout, last pull left stale). toZcodeEntry now applies a per-event timeout table (SessionStart 180s, Stop/UserPromptSubmit 60s, PostToolUse 30s) instead of inheriting the shell-hook defaults.
On Windows, CreateProcess resolves bare `bash` to System32's WSL launcher before any PATH directory. The WSL side has a different $HOME (no teamai state) and often no Node >= 20, so the spawned hook no-ops or dies — observed on a real machine: hook.run.failed for every ZCode hook while the same payload succeeded through Git Bash. On win32, render entries as cmd /c <payload> (cmd.exe always exists in System32; the npm .cmd shim resolves via PATHEXT). POSIX entries keep bash -lc. Payload stays verbatim in args[1] on both variants, keeping the manifest-invariant that fixed the team-hook matching bug.
Summary
~/.zcode/skills/, subagents (Claude-style Markdown) →~/.zcode/agents/, user-scope MCP →~/.agents/mcp.json, hooks →~/.zcode/cli/config.jsonzcodehook format: ZCode nests Claude-shaped hooks underhooks.eventsand gates them onhooks.enabled(config-file hooks are disabled by default, so the writer forces it on)process-type hook entries with network-scale timeouts (a session-start dispatch carries a pull to the team host), store the payload verbatim inargs[1], and omit wildcard matchers (ZCode matchers are case-sensitive regexes;*is an invalid pattern that never matches)reconcileZcodeFormatstrips schema-unknown keys from the hooks block (strict schema drops the entire block otherwise) and only forceshooks.enabled: trueon install — removal preserves the runner state the user choseCloses #461.
Context: ZCode's hooks mechanism is migrating
ZCode desktop is moving from config-file-declared hooks toward settings-page-managed
"installed" hooks (with a trust/review flow,
zcode hooks trustcommand family, and aper-session snapshot — changes take effect in new sessions). In the current desktop build:
hooks.eventsin~/.zcode/cli/config.json) are listed under a"legacy" group in the hooks settings page, with an import path into the new store
entire block to be dropped (
config_file_invaliddiagnostic,hookCount: 0, no hookexecution), while the settings page still lists the entries as if they were active
bashto System32's WSL launcher before anyPATH directory, and the WSL side has a different
$HOME(no teamai state) and often noNode ≥ 20 — so process entries route through
cmd /cinsteadThat is what this fix addresses: since teamai merges into the user's existing config, one
hand-added key would previously stay poisoned forever.
reconcileZcodeFormatnow keepsonly the keys the schema knows, so the config self-heals on the next sync.
Why
Observed on a real Windows machine (ZCode desktop, CLI 0.16.5): after a hand-edit added
"description": "..."insidehooks, the CLI loggedconfig_file_invalid — hooks: Unrecognized key: "description", reportedhookCount: 0,and executed no hooks. teamai's merge semantics had faithfully preserved the key across
every sync, so the config stayed in that state. Keeping only schema-known keys makes the
config recover automatically.
Validation
Unit / build
npx vitest run src/__tests__/zcode.test.ts(10/10, incl. healing + team-hook lifecycle + removal-state cases)npm run typecheck,npm run buildReal CLI end-to-end (isolated HOME, Windows 10.0.22631, Node v22.22.1, CLI built from this branch):
<HOME>/.zcode/cli/config.jsonwith a poisoned hooks block — unknowndescriptionkey,enabled: false, one pre-existing user hook(
process / echo user-existing-hook), plus unrelatedpluginsstatenode dist/index.js hooks inject→descriptionkey dropped;hooks.enabledflipped totruepluginsstate preserved verbatimhooks inject→ file byte-identical (md5 unchanged) — idempotentnode dist/index.js hooks remove→ teamai entries stripped; user hook andpluginspreserved(unit mirror: zcode.test.ts covers heals/inject/idempotent/remove)
Real-machine migration incident (ZCode desktop 3.9.2 / CLI 0.16.5):
descriptionkey): CLI logconfig_file_invalid — hooks: Unrecognized key: "description",hookCount: 0, no hook executionhookCount: 6,no
config_file_invalid; hooks executed on session events —session_startrecorded,the auto-pull completed (
teamai statuslast pull refreshed; a stale remote commit synced),and
prompt_submit/tool_use/stopwere recorded through subsequent sessionsBoundary
mcp.serverskey inside.zcode/config.json, which the Claude writer cannot emit)possible follow-up once its storage/format is stable and documented
Type of Change
Test Plan
npx tsc --noEmitpassesnpx vitest runpassesRelated Issues
Closes #461
Notes for Reviewers
~/.zcode/cli/config.jsonis shared with ZCode's own state (plugins etc.) —reconcileZcodeFormatmerges the document and only ever toucheshooks.enabled/hooks.events.<Event>, plus dropping schema-unknown keys.because ZCode hook groups carry no description field; both entry variants (posix
bash -lc/ win32cmd /c) carry the payload verbatim atargs[1], keeping onecommand representation across the entry, the manifest, and the desired set.
the migrating hooks mechanism, not a judgement on its design.