Assert claude honours the regenerated plugin manifest; correct §4.4's version-stale claim (refs #68) - #275
Merged
Merged
Conversation
…, 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
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