The audit swept every packages/*/* plugin for the new AGENTS.md
convention (no hardcoded tunables in plugins) and exposes each finding
as a defaulted, validated Config field. Defaults are the previously
hardcoded values throughout, so no deployment or golden changes.
- tool-fs (had NO Config): readLimit, readMaxLineLength, readMaxBytes,
readStreamMinSize. The caps thread through ReadToolCaps/ReadWindow —
read-render already documented that the consumer applies the caps, so
they become explicit per-request fields.
- tool-web: searchMaxResults (WEB_SEARCH_MAX_RESULTS stays as the
schemastery default). Also fixes the stale GREP_LIMIT references in
search.ts and the web-capability-seam RFC (no such constant exists).
- bash-local: graceMs (SIGTERM->SIGKILL escalation grace). The
RunInternals.graceMs test seam is gone: graceMs is now a required
SpawnSpec field filled from config, so tests exercise the real
config path and the defaults live in exactly one place.
- subagent-acp: disposeEofGraceMs / disposeGraceMs. The AcpRunSpec
fields become required for the same one-defaulting-layer reason.
- session-persistence-sqlite: journalMode ('wal' default; the
rollback-journal modes serve filesystems where WAL's shared-memory
files do not work, e.g. network mounts).
- hooks-claude + hooks-codex: stderrSummaryMaxChars for the persisted
hook/result stderr summary. The duplicated summarize() helpers merge
into hook-protocol's summarizeStderr(stderr, maxChars), beside the
HookResultRecord field it feeds, with the bound parameterized the
same way runHook's defaultTimeoutMs already is.
- compact-basic: charsPerToken for the token estimator (default 4, the
English-text heuristic; CJK-heavy deployments need ~1-2 or compaction
fires far too late). Also corrects the BasicCompactService class doc,
which claimed defaults the required-field config never had.
- fs-local: deletes the dead STREAM_MIN_SIZE constant and the dead
FsIoInternals.streamMinSize seam — the read-routing bound lives in
the consumer (tool-fs), where it is now config. This is item 1 of
the proposed prune-write-only-fs-surface RFC, annotated accordingly.
Every new field gets range validation (following the existing
assertPositiveFinite pattern), a README row, and tests covering the
configured behavior, the schema default, and load-time rejection.
@deepseek-ai/dsh-hook-protocol
The shared core of the Claude Code / Codex hook wire protocol. NOT a cordis plugin — it registers nothing and injects nothing. It is a library of dialect-neutral primitives the two bridge plugins (@deepseek-ai/dsh-hooks-claude, @deepseek-ai/dsh-hooks-codex) import so neither re-implements the identical halves of the protocol.
Why a shared lib at all: Codex deliberately reimplements a subset of the Claude Code hook protocol — the same hooks.json matcher-group shape, the same exit-code/stdout output contract, the same command-hook execution model. The genuinely-shared parts live here; each bridge owns only what differs.
What's shared (here) vs. per-dialect (the bridges)
| Concern | Here (dsh-hook-protocol) |
The bridge (dsh-hooks-claude / -codex) |
|---|---|---|
| Matcher test | matchesMatcher(pattern, query, mode) — literal-or-regex by mode |
picks its mode (claude = literal-or-regex, codex = always regex) |
| Run a hook | runHook(bash, hook, opts, now) — stdin payload + env via ctx.bash, decode |
builds the per-event stdin payload + the dialect's env |
| Decode output | parseHookOutput(exit, stdout, stderr) → neutral HookOutput |
maps the neutral HookOutput onto a seam-specific typed Decision |
| Merge N hooks | mergeHookOutputs(outputs) → most-restrictive MergedHookOutcome |
— |
| Durable record | appendHookInvoked / appendHookResult (hook/* session events) |
calls them around each invocation |
Primitives
matchesMatcher(matcher, query, mode)— match-all on absent/''/'*';claudemode treats a pure[A-Za-z0-9_|]+pattern as a literal (pipe = exact-match alternation) and anything else as a regex;codexmode is always an unanchored regex. An invalid regex matches nothing (never throws).runHook(bash, hook, options, now)— serializeoptions.payloadto the hook's stdin (with a trailing newline iffoptions.trailingNewline), mergeoptions.envafter the executor's credential scrub (thedsh-bashtrusted-plugin surface), honor the hook'stimeoutSec(elsedefaultTimeoutMs), and decode the result (threadingoptions.expectedEventNameto the codec). Never throws: an executor rejection (infra fault) becomes aHookOutputwithexitCode: undefined(a non-blocking error).nowis injected for testable durations.parseHookOutput(exitCode, stdout, stderr, expectedEventName?)— the exit-code + structured-stdout codec. Exit0→ parse JSON stdout (lenient: non-JSON is left for the bridge); exit2→ blocking error,stderris the block reason (surfaced asdecision: 'block'); other → non-blocking error.hookSpecificOutput.permissionDecision(allow/deny/ask) overrides a legacy top-leveldecision;additionalContext/updatedInput/systemMessage/continue/stopReason/suppressOutputare parsed too. The schemas key thehookSpecificOutputblock byhookEventName, so passingexpectedEventName(the firing event) DISCARDS a block whosehookEventNamenames a different event — or omits it entirely — its event-scoped fields don't take effect (aPreToolUseblock on aStophook is malformed, and so is a discriminator-less block that would otherwise apply to any event), while the event-agnostic top-level fields still apply. Pure and total.mergeHookOutputs(outputs)— fold the results of every hook that matched one point: permission precedence deny > ask > allow, halt sticky on the firstcontinue:false, block reasons joined with\n\n,additionalContext/systemMessagesaccumulated in order.
hook/* session events
Declaration-merged into SessionEventMap (log-only, like compact/* — NOT a SurfaceEventType, no surfaceOp):
hook/invoked—{ turn, point, dialect, matcher?, handlerId }: a hook command ran.hook/result—{ turn, point, handlerId, decision, exitCode?, stderrSummary?, durationMs }: its outcome, paired byhandlerId.
Like every event they must sit inside an open turn. The mid-turn points (PreToolUse/PostToolUse/UserPromptSubmit/Stop) fire inside the loop's open turn by construction; SessionStart gets no hook/* record (its injected context/message is the durable evidence) — see the hooks RFC.
Input rewrite is parsed but not honored
HookOutput.updatedInput carries a hook's requested tool-input rewrite (CC updatedInput), but the harness does not honor it yet — input rewrite is a deferred consistency-design problem (the pre-tool-input-rewrite RFC). A bridge logs + warns when a hook sets it. See src/types.ts for the full contracts.