In hooks/tsup.config.ts, add an esbuild plugin (externalize-sibling-hooks) that marks sibling ./*.mjs imports as external before tsup's noExternal catch-all runs. The previous config used noExternal: [/.*/] which overrode the external array, causing sibling hooks to be inlined with their side-effecting isMainModule() guards — producing invalid concatenated JSON output like "{}{}". The fix uses build.onResolve to intercept ./foo.mjs imports matching the hookExternalSet and return { external: true }. npm dependencies (minisearch, etc.) remain bundled inline.
In hooks/src/user-prompt-submit-skill-inject.mts line 581, change the lexical check from process.env.VERCEL_PLUGIN_LEXICAL_PROMPT === "1" to !== "0" so lexical stemmer fallback is on by default. Update the CLAUDE.md env var table to show default as "on" with "0" to disable.
Rebuild all compiled hooks via bun run build:hooks.
Claude session-start-profiler no longer requires CLAUDE_ENV_FILE to persist likely skills or greenfield state.
Those values now come from the hook input session ID and are written to session files,
while Claude env exports remain only for the other profiler vars.
Verified: bun run build:hooks
Verified: bun test tests/session-start-profiler.test.ts
Swarm-Agent: codex-profiler-env-cleanup
Make the session-start profiler resolve binaries from PATH safely before
invoking them, cap the version-check subprocesses at 3 seconds, and
avoid crashing when npm or agent-browser is missing.
Also expand the outdated Vercel CLI guidance to include the pnpm global
upgrade path and cover the new skip/timeout behavior in profiler tests.
Verified: bun test tests/session-start-profiler.test.ts
Swarm-Agent: codex-profiler-harden-split-2-v2
Replace empty catch blocks in hook-env and session-start-profiler with\nstructured debug logging using the shared hook logger.\nAlso make the Vercel CLI update check compare numeric version\nsegments so 1.9.0 correctly sorts below 1.10.0.\n\nVerified: bun test tests/session-start-profiler.test.ts\nHow to test: bun test tests/session-start-profiler.test.ts\nSwarm-Agent: codex-profiler-harden-split-1
Hash invalid session IDs before constructing dedup temp paths so crafted stdin values cannot smuggle traversal segments into recursive claim cleanup. Shared temp-path resolution now verifies the resolved target stays under tmpdir, and seen-skills tests cover both stable safe IDs and hashed invalid IDs.
Verified: bun test tests/session-start-seen-skills.test.ts
How to test: bun test tests/session-start-seen-skills.test.ts
Swarm-Agent: codex-path-traversal
Parallel PreToolUse hooks were racing on the same seen-skills file, causing 2-6x
duplicate injections. UserPromptSubmit hook didn't share dedup state at all.
Fix by implementing atomic per-skill claim files (Oracle session: fix-dedup-race-condition-plan):
1. In hooks/src/hook-env.mts, add tryClaimSessionKey(sessionId, kind, key) using
openSync(path, 'wx') for atomic exclusive create. Add listSessionKeys() to read
claim dir, syncSessionFileFromClaims() to derive the txt snapshot, and
removeSessionClaimDir() for cleanup. Claim files live in
<tmpdir>/vercel-plugin-<sessionId>-seen-skills.d/<skill-name>.
2. In hooks/src/patterns.mts, add mergeSeenSkillStates(...values) to union env var,
session file, and claim dir state. Add serializeSeenSkills(set) helper.
3. In hooks/src/pretooluse-skill-inject.mts, load merged state from env+file+claims.
Before injecting each skill, call tryClaimSessionKey — first process wins, others
get EEXIST and skip. Use same claim path for once-only keys (agent-browser warning,
vercel-env-help). Pass sessionId through InjectOptions.
4. In hooks/src/user-prompt-submit-skill-inject.mts, preserve sessionId from parsed
input, load merged seen state, pass sessionId to injection flow so it uses the
shared claim backend. Skills injected via promptSignals now visible to PreToolUse.
5. In hooks/src/session-end-cleanup.mts, add rmSync deletion of claim directories.
6. Update CLAUDE.md dedup contract to document atomic claim mechanism.
7. Bump .plugin/plugin.json to 0.6.0, rebuild.
PreToolUse and PostToolUse dedup state was previously process-local, which broke across hook subprocess invocations.\nThis change adds session-scoped temp-file persistence and threads session_id through the relevant flows.\n\nPreToolUse now falls back to per-session seen-skills file state and writes back on every appendSeenSkill update.\nPostToolUse now checks persisted validated-files state and writes updates after marking files validated.\n\nVerified: bun run build:hooks\nVerified: bun test tests/pretooluse-skill-inject.test.ts tests/posttooluse-validate.test.ts\nSwarm-Agent: codex-dedup-core
The 0.5.4 release added session-scoped temp files for dedup persistence, but this
was wrong — CLAUDE_ENV_FILE env vars ARE propagated to hook subprocesses by Claude
Code. The temp file approach littered tmpdir with hundreds of small files.
Revert to the original env-var-only mechanism:
1. Remove readSessionFile/writeSessionFile/dedupFilePath from hooks/src/hook-env.mts
2. Remove all writeSessionFile calls from pretooluse-skill-inject.mts — revert to
pure process.env.VERCEL_PLUGIN_SEEN_SKILLS = appendSeenSkill(...) pattern
3. Remove sessionId from InjectOptions and the injectSkills call site
4. Remove sessionId from posttooluse-validate.mts ParsedInput — revert
isAlreadyValidated/markValidated to pure env-var signatures
5. Revert all test expectations back to "env-var" and "memory-only" strategies
6. Restore CLAUDE.md dedup contract to env-var-only documentation
Skill dedup is completely broken because CLAUDE_ENV_FILE vars are only available to Bash
tool commands, not hook subprocesses. Each PreToolUse invocation starts a fresh Node.js
process that never sees VERCEL_PLUGIN_SEEN_SKILLS.
To fix this:
1. In hooks/src/hook-env.mts, add readSessionFile(sessionId, kind) and
writeSessionFile(sessionId, kind, value) helpers that read/write
/tmp/vercel-plugin-<sessionId>-<kind>.txt for persistent dedup state.
2. In hooks/src/pretooluse-skill-inject.mts, read seen skills from the session file
at startup using readSessionFile(sessionId, "seen-skills"). Add a persistSeen()
closure inside injectSkills() that calls both appendSeenSkill and writeSessionFile.
Replace all process.env.VERCEL_PLUGIN_SEEN_SKILLS = appendSeenSkill(...) calls with
persistSeen() or inline writeSessionFile calls. Add "file" as the primary dedup
strategy when session_id is present, falling back to "env-var" then "memory-only".
Pass sessionId through InjectOptions to injectSkills().
3. In hooks/src/posttooluse-validate.mts, add sessionId to ParsedInput and extract it
from input.session_id in parseInput(). Pass sessionId to isAlreadyValidated() and
markValidated(), which should read/write via readSessionFile/writeSessionFile for
the "validated-files" kind.
4. Update tests: pretooluse-skill-inject.test.ts — rename "no env var" test to expect
file-based dedup persistence, update strategy assertions from "env-var" to "file",
use omitSessionId for memory-only test. subagent-fresh-env.test.ts — use a fresh
session ID for subagent calls, update strategy expectations to "file".
session-timeline-subagent.test.ts — add sessionId option to runHookEnv, use a
different session ID for the subagent call.
5. Update CLAUDE.md dedup contract section to document file-based mechanism.
6. Bump .plugin/plugin.json version to 0.5.4, run bun run build.
When hooks run as child processes, process.cwd() can point at the plugin root\ninstead of the user project.\n\nThis change threads cwd from PreToolUse stdin JSON through parseInput()\ninto appendAuditLog(), and uses it as fallback when CLAUDE_PROJECT_ROOT\nis not set.\n\nVerified: bun run build:hooks\nVerified: bun test tests/pretooluse-skill-inject.test.ts\nSwarm-Agent: codex-p0-audit-path
Skill injections now append JSONL audit records for post-run debugging and traceability.
Audit path resolves from VERCEL_PLUGIN_AUDIT_LOG_FILE with an opt-out mode and a project-local default.
Verified: bun run build:hooks
Verified: bun test tests/subagent-fresh-env.test.ts
Swarm-Agent: codex-audit-log