Skip to content

fix(plugins): stop hook tests, parser_reader and book-distill misrouting plugin data #6072

Description

@kyle-sexton

Problem

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):

  1. 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.
  2. harness-ops parser_reader.py takes a prefix match. plugins/harness-ops/skills/inventory/scripts/parser_reader.py:95-97 accepts the inherited value when Path(env).name.startswith("harness-ops"), which also accepts a lookalike such as harness-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.
  3. 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/main b3691e5:

Claimed by the item, not reproduced:

  • "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.
  • A third skip-notice key, 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.
  • 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

  1. 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.
  2. parser_reader: replace the startswith with 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 equals harness-ops or starts with harness-ops-.
  3. 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.

Constraints and gotchas

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".

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent-readyFully specified and briefed; eligible for autonomous pickup from the frontier.priority: mediumReal value, no hard deadline; normal backlog flow.work-class: scopedA briefed fix or small feature; blast radius bounded by the brief, tests exist.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions