You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
On this machine the codex plugin (openai-codex 1.0.6, scripts/session-lifecycle-hook.mjs:80) appends its own CLAUDE_PLUGIN_DATA to CLAUDE_ENV_FILE at SessionStart, so every Bash tool call sees ~/.claude/plugins/data/codex-openai-codex. The plugins reference ("Where each variable resolves") says the Bash tool's environment does not carry this variable at all, while hook processes receive their own plugin's value. Any value a Bash-run script sees therefore came from some other plugin's env file. #6004 (merged) fixed knowledge's video-digest and course-digest; #6065 (draft) fixes repo-hygiene, code-metrics, harness-ops, session-flow, source-control, ai-briefing and context-budget. Three sites remain on current main (b3691e5):
Hook test suites run the hook with the inherited value. When a suite is run from a Claude Code Bash call, the hook under test writes its state into whatever CLAUDE_PLUGIN_DATA the shell holds:
plugins/markdown-format/hooks/markdown-format.test.sh:169-197: run_hook / run_hook_env pass only CLAUDE_PLUGIN_OPTION_MARKDOWN_FORMAT_ENABLED and do not set or unset CLAUDE_PLUGIN_DATA, so markdown-format.sh:1264-1290 writes finding-digests/no-session.<sha> (and scope-1.<sha>) under the inherited dir. Only a few cases (:969, :995, :1031, :1059, :1081) pass their own data dir.
plugins/source-control/hooks/pr-body-linkage-gate.test.sh:692-715: the missing-jq case runs the hook with the inherited value, so the vendored hook-utils.sh skip-notice helper (lib/hook-utils.sh:301-315) writes skip-notices/source-control-pr-body-linkage-gate-jq.test.no-agent there. The file never mentions CLAUDE_PLUGIN_DATA.
The test runner scripts/run-plugin-tests.sh:67-80 already clears inherited GIT_* variables for fixture isolation but leaves CLAUDE_PLUGIN_DATA alone.
knowledge book-distill spokes carry a literal ${CLAUDE_PLUGIN_DATA}.plugins/knowledge/skills/book-distill/context/templates.md:7, :37, :69 and templates/checklist.md:3. These files are opened with the Read tool, which returns bytes unsubstituted, so an agent that pastes the path into Bash expands it to the codex dir (or to nothing). The same skill's SKILL.md body (:30, :57, :96) is substituted at load and is fine. fix: keep Bash-run plugin scripts out of other plugins' data directories #6065's body notes these lines were left for a follow-up.
Evidence
Verified this pass, read-only on origin/mainb3691e5:
Each line above read with git show origin/main:<path>.
~/.claude/plugins/data/codex-openai-codex/ listed read-only: finding-digests/ holds 50 files named no-session.* / scope-1.* (newest 2026-09-25), and skip-notices/ holds 4 files whose keys contain test or no-session. A harness-run hook always has a session id, so these came from test runs.
"guardrails commit-msg hook writes convention-pattern/ under the inherited value." The writer is plugins/guardrails/hooks/block-convention-violation.sh:191-192, a PreToolUse hook, not a git commit-msg hook. Run by the harness, it receives guardrails' own value. The 1177 cache files in the codex dir are dated 2026-09-15 to 2026-09-25 and are keyed mostly by temp test repos. Since 29a36c9 (fix(disk-hygiene): trust uutils coreutils heads; isolate three suites from session env #5180, 2026-09-28), plugins/guardrails/hooks/guardrails-test-helpers.sh:24 unsets CLAUDE_PLUGIN_DATA for every guardrails suite, and scripts/check-guardrails-ps-differential.sh:267 sets its own. No product change is needed there.
Untraced: skip-notices/bash-format-jq.no-session.no-agent. bash-format.test.sh passes its own data dir in the cases found (:579, :626, :652, :667, :860). The case that wrote this notice was not identified; the runner-level unset below covers it either way.
Inferred, not observed: no leaking write has happened recently. Both writers delete their own entries older than 7 days on each new write (hook-utils.sh skip-notices, markdown-format.sh:1288), yet files dated 2026-09-23 remain, so no leaking write has run in about a week. The code paths above still leak whenever a suite runs from a Claude Code Bash call.
Proposed approach
Test isolation, two layers, both following existing precedent:
scripts/run-plugin-tests.sh: add CLAUDE_PLUGIN_DATA to the existing unset at :80, so every suite the runner spawns sees it unset, as on CI. A suite that needs a data dir already sets its own (for example bash-format.test.sh:579).
Per suite, for runs outside the runner: at the top of markdown-format.test.sh and pr-body-linkage-gate.test.sh, unset CLAUDE_PLUGIN_DATA, the same line as guardrails-test-helpers.sh:24. Cases that exercise the digest or notice store keep setting their own sandbox.
Alternatives considered: (a) point CLAUDE_PLUGIN_DATA at a per-run sentinel dir and fail when a suite writes there. Rejected: a set value latches hook::notice_once across cases (guardrails-test-helpers.sh:21-22), so suites that expect a notice on every case would take different paths than on CI. (b) A lexical gate like scripts/check-test-tmp-cleanup.sh requiring each hook suite to mention the variable. Rejected: markdown-format.test.sh mentions it 22 times and still leaks.
book-distill: follow the video-digest precedent (plugins/knowledge/skills/video-digest/SKILL.md:195-196). In the spokes, write <plugin-data>. In the SKILL.md body, add one line saying <plugin-data> is ${CLAUDE_PLUGIN_DATA} (substituted at load) and must be put in place before any path is used.
Files: scripts/run-plugin-tests.sh (plus run-plugin-tests.test.sh), plugins/markdown-format/hooks/markdown-format.test.sh, plugins/source-control/hooks/pr-body-linkage-gate.test.sh, plugins/harness-ops/skills/inventory/scripts/parser_reader.py and test_parser_reader.py, plugins/knowledge/skills/book-distill/SKILL.md, context/templates.md, templates/checklist.md, and each touched plugin's plugin.json and CHANGELOG.md.
Acceptance criteria
run-plugin-tests.sh unsets CLAUDE_PLUGIN_DATA before spawning suites. run-plugin-tests.test.sh proves a spawned fixture suite sees it unset when the caller exported one.
Running markdown-format.test.sh and pr-body-linkage-gate.test.sh directly with CLAUDE_PLUGIN_DATA pointing at an empty temp directory leaves that directory empty. On current main it does not.
parser_reader.deps_base(None) ignores CLAUDE_PLUGIN_DATA=/x/harness-opsx-foo and accepts /x/harness-ops-melodic-software. Tests in test_parser_reader.py cover both, and the reject test fails on current main.
git grep -n 'CLAUDE_PLUGIN_DATA' -- plugins/knowledge/skills/book-distill/context plugins/knowledge/skills/book-distill/templates returns nothing, and SKILL.md defines <plugin-data> as the substituted ${CLAUDE_PLUGIN_DATA}.
Version bump and CHANGELOG entry for harness-ops and knowledge (shipped files change). markdown-format and source-control change only test files; bump them only if repo practice requires it for test-only edits. check-changelog-parity.sh --check --check-order --check-bump origin/main passes.
The skip-notice and digest stores prune by age (find ... -mtime +7 -delete) inside whatever directory they resolve. A sentinel dir must be a fresh temp dir, never a real data dir.
Do not change hook runtime code to second-guess CLAUDE_PLUGIN_DATA. Harness-run hooks receive the correct value per the docs table. The fault is in Bash-run callers.
Out of scope: moving the state already in codex-openai-codex/ (user-scope cleanup, handled separately), and the upstream codex env-file export (whether to report it upstream is the maintainer's call).
Context
Source: local handoff item 20261003-074552-plugin-data-env-leftovers-after-6065.md (retired into this issue). Related: #5982, #6004 (merged), #6065 (draft), #5180 (guardrails test isolation), #4465 (removed ready-evidence hook). Convention: docs/conventions/on-demand-dependencies/README.md "Finding the plugin data directory".
Problem
On this machine the codex plugin (openai-codex 1.0.6,
scripts/session-lifecycle-hook.mjs:80) appends its ownCLAUDE_PLUGIN_DATAtoCLAUDE_ENV_FILEat SessionStart, so every Bash tool call sees~/.claude/plugins/data/codex-openai-codex. The plugins reference ("Where each variable resolves") says the Bash tool's environment does not carry this variable at all, while hook processes receive their own plugin's value. Any value a Bash-run script sees therefore came from some other plugin's env file. #6004 (merged) fixed knowledge's video-digest and course-digest; #6065 (draft) fixes repo-hygiene, code-metrics, harness-ops, session-flow, source-control, ai-briefing and context-budget. Three sites remain on current main (b3691e5):CLAUDE_PLUGIN_DATAthe shell holds:plugins/markdown-format/hooks/markdown-format.test.sh:169-197:run_hook/run_hook_envpass onlyCLAUDE_PLUGIN_OPTION_MARKDOWN_FORMAT_ENABLEDand do not set or unsetCLAUDE_PLUGIN_DATA, somarkdown-format.sh:1264-1290writesfinding-digests/no-session.<sha>(andscope-1.<sha>) under the inherited dir. Only a few cases (:969,:995,:1031,:1059,:1081) pass their own data dir.plugins/source-control/hooks/pr-body-linkage-gate.test.sh:692-715: the missing-jq case runs the hook with the inherited value, so the vendoredhook-utils.shskip-notice helper (lib/hook-utils.sh:301-315) writesskip-notices/source-control-pr-body-linkage-gate-jq.test.no-agentthere. The file never mentionsCLAUDE_PLUGIN_DATA.scripts/run-plugin-tests.sh:67-80already clears inheritedGIT_*variables for fixture isolation but leavesCLAUDE_PLUGIN_DATAalone.parser_reader.pytakes a prefix match.plugins/harness-ops/skills/inventory/scripts/parser_reader.py:95-97accepts the inherited value whenPath(env).name.startswith("harness-ops"), which also accepts a lookalike such asharness-opsx-foo. fix(knowledge): resolve the plugin data dir explicitly instead of trusting the Bash env #6004 and fix: keep Bash-run plugin scripts out of other plugins' data directories #6065 require an exact plugin match (<plugin>or<plugin>-*as the last path segment), each with a "does not take a prefix match" test.${CLAUDE_PLUGIN_DATA}.plugins/knowledge/skills/book-distill/context/templates.md:7,:37,:69andtemplates/checklist.md:3. These files are opened with the Read tool, which returns bytes unsubstituted, so an agent that pastes the path into Bash expands it to the codex dir (or to nothing). The same skill'sSKILL.mdbody (:30,:57,:96) is substituted at load and is fine. fix: keep Bash-run plugin scripts out of other plugins' data directories #6065's body notes these lines were left for a follow-up.Evidence
Verified this pass, read-only on
origin/mainb3691e5:git show origin/main:<path>.~/.claude/plugins/data/codex-openai-codex/listed read-only:finding-digests/holds 50 files namedno-session.*/scope-1.*(newest 2026-09-25), andskip-notices/holds 4 files whose keys containtestorno-session. A harness-run hook always has a session id, so these came from test runs.CLAUDE_PLUGIN_DATAto the process, and the Bash tool does not receive it.CLAUDE_PLUGIN_DATA,parser_readerandbook-distill. No duplicate. Closest: knowledge: video-digest reads its dependency directory only from an environment variable the Bash tool never carries, so its pipeline and preflight fail before any digest work #5982 (video-digest, the original report), fix(knowledge): resolve the plugin data dir explicitly instead of trusting the Bash env #6004, fix: keep Bash-run plugin scripts out of other plugins' data directories #6065.Claimed by the item, not reproduced:
convention-pattern/under the inherited value." The writer isplugins/guardrails/hooks/block-convention-violation.sh:191-192, a PreToolUse hook, not a git commit-msg hook. Run by the harness, it receives guardrails' own value. The 1177 cache files in the codex dir are dated 2026-09-15 to 2026-09-25 and are keyed mostly by temp test repos. Since 29a36c9 (fix(disk-hygiene): trust uutils coreutils heads; isolate three suites from session env #5180, 2026-09-28),plugins/guardrails/hooks/guardrails-test-helpers.sh:24unsetsCLAUDE_PLUGIN_DATAfor every guardrails suite, andscripts/check-guardrails-ps-differential.sh:267sets its own. No product change is needed there.source-control-pr-ready-evidence-head.*, came from a hook that refactor(source-control)!: remove the skill-evidence system and the review-lane guards #4465 (aaf4c44, 2026-09-24) removed. Nothing to fix.skip-notices/bash-format-jq.no-session.no-agent.bash-format.test.shpasses its own data dir in the cases found (:579,:626,:652,:667,:860). The case that wrote this notice was not identified; the runner-level unset below covers it either way.Inferred, not observed: no leaking write has happened recently. Both writers delete their own entries older than 7 days on each new write (
hook-utils.shskip-notices,markdown-format.sh:1288), yet files dated 2026-09-23 remain, so no leaking write has run in about a week. The code paths above still leak whenever a suite runs from a Claude Code Bash call.Proposed approach
scripts/run-plugin-tests.sh: addCLAUDE_PLUGIN_DATAto the existingunsetat:80, so every suite the runner spawns sees it unset, as on CI. A suite that needs a data dir already sets its own (for examplebash-format.test.sh:579).markdown-format.test.shandpr-body-linkage-gate.test.sh,unset CLAUDE_PLUGIN_DATA, the same line asguardrails-test-helpers.sh:24. Cases that exercise the digest or notice store keep setting their own sandbox.CLAUDE_PLUGIN_DATAat a per-run sentinel dir and fail when a suite writes there. Rejected: a set value latcheshook::notice_onceacross cases (guardrails-test-helpers.sh:21-22), so suites that expect a notice on every case would take different paths than on CI. (b) A lexical gate likescripts/check-test-tmp-cleanup.shrequiring each hook suite to mention the variable. Rejected:markdown-format.test.shmentions it 22 times and still leaks.startswithwith the fix: keep Bash-run plugin scripts out of other plugins' data directories #6065 shape: accept the inherited value only when the last path segment equalsharness-opsor starts withharness-ops-.plugins/knowledge/skills/video-digest/SKILL.md:195-196). In the spokes, write<plugin-data>. In the SKILL.md body, add one line saying<plugin-data>is${CLAUDE_PLUGIN_DATA}(substituted at load) and must be put in place before any path is used.Files:
scripts/run-plugin-tests.sh(plusrun-plugin-tests.test.sh),plugins/markdown-format/hooks/markdown-format.test.sh,plugins/source-control/hooks/pr-body-linkage-gate.test.sh,plugins/harness-ops/skills/inventory/scripts/parser_reader.pyandtest_parser_reader.py,plugins/knowledge/skills/book-distill/SKILL.md,context/templates.md,templates/checklist.md, and each touched plugin'splugin.jsonandCHANGELOG.md.Acceptance criteria
run-plugin-tests.shunsetsCLAUDE_PLUGIN_DATAbefore spawning suites.run-plugin-tests.test.shproves a spawned fixture suite sees it unset when the caller exported one.markdown-format.test.shandpr-body-linkage-gate.test.shdirectly withCLAUDE_PLUGIN_DATApointing at an empty temp directory leaves that directory empty. On current main it does not.parser_reader.deps_base(None)ignoresCLAUDE_PLUGIN_DATA=/x/harness-opsx-fooand accepts/x/harness-ops-melodic-software. Tests intest_parser_reader.pycover both, and the reject test fails on current main.git grep -n 'CLAUDE_PLUGIN_DATA' -- plugins/knowledge/skills/book-distill/context plugins/knowledge/skills/book-distill/templatesreturns nothing, andSKILL.mddefines<plugin-data>as the substituted${CLAUDE_PLUGIN_DATA}.check-changelog-parity.sh --check --check-order --check-bump origin/mainpasses.Constraints and gotchas
~/.claude/plugins/data. Give every case its own tempCLAUDE_PLUGIN_DATA(andHOMEwhere a fallback path is exercised), as fix: keep Bash-run plugin scripts out of other plugins' data directories #6065's tests do.find ... -mtime +7 -delete) inside whatever directory they resolve. A sentinel dir must be a fresh temp dir, never a real data dir.CLAUDE_PLUGIN_DATA. Harness-run hooks receive the correct value per the docs table. The fault is in Bash-run callers.codex-openai-codex/(user-scope cleanup, handled separately), and the upstream codex env-file export (whether to report it upstream is the maintainer's call).Context
Source: local handoff item 20261003-074552-plugin-data-env-leftovers-after-6065.md (retired into this issue). Related: #5982, #6004 (merged), #6065 (draft), #5180 (guardrails test isolation), #4465 (removed ready-evidence hook). Convention:
docs/conventions/on-demand-dependencies/README.md"Finding the plugin data directory".