Skip to content

Assert claude honours the regenerated plugin manifest; correct §4.4's version-stale claim (refs #68) - #275

Merged
vyskocilm merged 2 commits into
mainfrom
issue-68-plugin-hook-test
Aug 21, 2026
Merged

Assert claude honours the regenerated plugin manifest; correct §4.4's version-stale claim (refs #68)#275
vyskocilm merged 2 commits into
mainfrom
issue-68-plugin-hook-test

Conversation

@vyskocilm

Copy link
Copy Markdown
Contributor

Closes the residual #68 named: no test asserted an UNNAMED plugin's hook does not fire in a live run, with a named plugin's firing as control.

TestManifestGatesPluginHookFiring (host-level, -tags integration) drives the real claude binary against a two-plugin fixture, three rows:

  • row 3 BASELINE: both enabled, both in installed_plugins.json -> both fire. Positive control; without it a green negative could mean "nothing ran".
  • row 2 ASSERTION: both enabled in settings.json, manifest is real policy.FilterInstalledPlugins output naming only "named" -> named fires, unnamed does NOT. @claude binds ~/.claude/plugins, and a plugin manifest is a command table #68's gate, on the binary.
  • row 4 ENABLEMENT: manifest names named, enabledPlugins empty -> nothing fires.

Hermetic, free, no network, no credentials: SessionStart fires at session INIT before the API turn, so a dead endpoint (127.0.0.1:9) observes firing. Fresh fixture per run — claude mutates plugin state between runs, a reused fixture gives a false negative. Skips where claude absent (t.Skip, not skipOrFail: "is Claude Code installed" is not a sandbox capability, same call the inventory test makes for a missing ~/.claude.json).

MEASURED claude 2.1.238, and it corrects §4.4. A plugin hook fires only when BOTH named in installed_plugins.json AND enabled in settings.json's enabledPlugins — two AND-gates. §4.4 said "auto-loads regardless of enabledPlugins": false on 2.1.238. enabledPlugins moved from ~/.claude.json to settings.json between versions — a claim about a third party's binary without its version went stale, the exact failure this file records. Both claims now carry the version.

snug closes the channel by TWO mechanisms on 2.1.238: the manifest regeneration (#68) AND the settings.json filter dropping enabledPlugins (claudesettings.go). #68 is defence-in-depth behind an enablement gate already there. Either alone stops the measured channel; state both because either can drift out from under the other.

Why host-level and not in-sandbox: inside @claude snug drops enabledPlugins, so nothing fires (row 4) and no positive control can exist there — a negative would pass because nothing ran. The control lives only where enabledPlugins survives: the host, against the binary directly. Stated in §4.4, claudeGuidance, base.toml, VERIFY 6g-bis.

Scope, kept narrow the way §4.4 is: covers the MANIFEST gate on the binary. Does NOT exercise the sandbox mount delivering the file inside a run — that is claudestagedset_test.go and TestClaudeJSONInsideCarriesNoHostProjectInventory.

Sites touched: new test/integration/pluginhooks_test.go; CLAUDE-SETTINGS.md §4.4; internal/cli/claude.go claudeGuidance; internal/profile/profiles/base.toml abuse block; VERIFY.md 6g-bis. No production code change — a test and four doc corrections.

make gate green; go test ./internal/cli green; the new test passes in ~19s and skips where claude is absent.

🤖 Generated with Claude Code

vyskocilm and others added 2 commits August 21, 2026 16:24
…, and correct §4.4's version-stale claim (refs #68)

#68 regenerates installed_plugins.json so only allowlisted plugins load. Unit
tests assert what snug WRITES; nothing asserted what the third-party binary DOES
with it — the part that drifts. TestManifestGatesPluginHookFiring drives real
claude against a two-plugin fixture, host-level, and asserts the gate on the
binary: a plugin enabled in settings.json but ABSENT from the regenerated
manifest does not fire its SessionStart hook (row 2), one present DOES (row 3,
the positive control), and with enabledPlugins empty nothing fires (row 4).

Hermetic and free: SessionStart fires at session INIT, before the API turn, so a
dead endpoint with no credentials observes firing. Fresh fixture per run — claude
mutates plugin state between runs and a reused fixture gives a false negative.
Skips where claude is absent (t.Skip, not skipOrFail: "is Claude Code installed"
is not a sandbox capability, same call the inventory test makes).

Measured claude 2.1.238: a hook needs BOTH the manifest entry AND enabledPlugins
in settings.json. §4.4 said "auto-loads regardless of enabledPlugins" — false
now; enabledPlugins moved from ~/.claude.json to settings.json between versions,
the exact staleness this file keeps recording. Both claims now carry the version.

snug closes the channel by TWO gates on this version: the manifest regeneration
(#68) AND the settings.json filter dropping enabledPlugins — #68 is
defence-in-depth behind an enablement gate already there. The in-sandbox form has
no positive control (snug drops enabledPlugins, so inside @claude nothing fires),
which is why the assertion is host-level; stated in §4.4, claudeGuidance,
base.toml, VERIFY 6g-bis.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vyskocilm
vyskocilm merged commit 46b7f7b into main Aug 21, 2026
5 checks passed
@vyskocilm
vyskocilm deleted the issue-68-plugin-hook-test branch August 21, 2026 16:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant