Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/actionlint/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "actionlint",
"version": "0.8.28",
"version": "0.8.29",
"description": "Lint GitHub Actions workflow files on edit via actionlint, surfacing findings as advisory context.",
"author": {
"name": "Melodic Software",
Expand Down
9 changes: 9 additions & 0 deletions plugins/actionlint/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
All notable changes to the `actionlint` plugin are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning.

## [0.8.29]

### Changed

- **The hook carries `if` filters, `Edit(**/.github/workflows/*.yml)` and
`Edit(**/.github/workflows/*.yaml)`.** Those are the only paths the hook checks, so a
Write/Edit of any other file no longer spawns it; behavior on workflow files is
unchanged.

## [0.8.28]

### Changed
Expand Down
4 changes: 3 additions & 1 deletion plugins/actionlint/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ your `PATH`.
via `additionalContext`; they never reject the edit. Make a commit hook or CI
your hard gate.
- **Scoped to workflows.** Only files matching `.github/workflows/*.yml` and
`.github/workflows/*.yaml` are linted. Other YAML is left alone.
`.github/workflows/*.yaml` are linted. Other YAML is left alone. The registration
carries the matching `if` filters (`Edit(**/.github/workflows/*.yml)` and the
`.yaml` twin), so a Write/Edit of any other file never starts a hook process for it.
- **External run-block linters disabled (`-shellcheck= -pyflakes=`).**
actionlint's embedded-bash ShellCheck and `shell: python` pyflakes
integrations are turned off. Each spawns a subprocess per `run:` block.
Expand Down
8 changes: 8 additions & 0 deletions plugins/actionlint/hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@
{
"type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/actionlint-check.sh",
"if": "Edit(**/.github/workflows/*.yml)",
"timeout": 15,
"statusMessage": "Checking workflow with actionlint..."
},
{
"type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/actionlint-check.sh",
"if": "Edit(**/.github/workflows/*.yaml)",
"timeout": 15,
"statusMessage": "Checking workflow with actionlint..."
}
Expand Down
2 changes: 1 addition & 1 deletion plugins/bash-format/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "bash-format",
"version": "0.7.29",
"version": "0.7.30",
"description": "Auto-format and lint shell scripts on edit via shfmt + ShellCheck, using the consuming repo's own .editorconfig and .shellcheckrc.",
"author": {
"name": "Melodic Software",
Expand Down
8 changes: 8 additions & 0 deletions plugins/bash-format/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
All notable changes to the `bash-format` plugin are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning.

## [0.7.30]

### Changed

- **The hook carries `if` filters, `Edit(*.sh)` and `Edit(*.bash)`.** These are exactly
the extensions the hook formats, so a Write/Edit of any other file no longer spawns it;
behavior on shell scripts is unchanged.

## [0.7.29]

### Changed
Expand Down
3 changes: 3 additions & 0 deletions plugins/bash-format/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ and `.editorconfig` for formatting. It ships no rules of its own.

## Behavior

- **Spawned only for shell files.** The hook is registered with the `if` filters
`Edit(*.sh)` and `Edit(*.bash)`, so a Write/Edit of any other file never starts a
hook process for it; the extension check inside the script is unchanged.
- **Lint on edit (always).** ShellCheck (`warning` severity and above) runs on
every edit. It is non-mutating; it only reports.
- **Format on edit (opt-in).** `shfmt` runs **only when an `.editorconfig`
Expand Down
8 changes: 8 additions & 0 deletions plugins/bash-format/hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@
{
"type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/bash-format.sh",
"if": "Edit(*.sh)",
"timeout": 15,
"statusMessage": "Formatting shell script..."
},
{
"type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/bash-format.sh",
"if": "Edit(*.bash)",
"timeout": 15,
"statusMessage": "Formatting shell script..."
}
Expand Down
2 changes: 1 addition & 1 deletion plugins/biome-format/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "biome-format",
"version": "0.6.28",
"version": "0.6.29",
"description": "Auto-format and lint JS/TS/JSX/JSON on edit via Biome, only when a biome.json governs the repo \u2014 using the consuming repo's own Biome config.",
"author": {
"name": "Melodic Software",
Expand Down
9 changes: 9 additions & 0 deletions plugins/biome-format/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
All notable changes to the `biome-format` plugin are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning.

## [0.6.29]

### Changed

- **The hook carries one `if` filter per extension it formats** (`Edit(*.ts)`,
`Edit(*.tsx)`, `Edit(*.js)`, `Edit(*.jsx)`, `Edit(*.mjs)`, `Edit(*.cjs)`, `Edit(*.mts)`,
`Edit(*.cts)`, `Edit(*.json)`, `Edit(*.jsonc)`), so a Write/Edit of any other file no
longer spawns it; behavior on those files is unchanged.

## [0.6.28]

### Changed
Expand Down
4 changes: 4 additions & 0 deletions plugins/biome-format/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ runs only when your repo has opted into Biome.

## Behavior

- **Spawned only for the files it formats.** The hook is registered with one `if`
filter per extension (`.ts`, `.tsx`, `.js`, `.jsx`, `.mjs`, `.cjs`, `.mts`, `.cts`,
`.json`, `.jsonc`), so a Write/Edit of any other file never starts a hook process
for it; the extension check inside the script is unchanged.
- **Opt-in on `biome.json`.** Biome runs **only when a `biome.json` or
`biome.jsonc` governs the edited file**, found by walking up from the file to
the repository root. A repo without a Biome config is left untouched rather
Expand Down
64 changes: 64 additions & 0 deletions plugins/biome-format/hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,70 @@
{
"type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/biome-format.sh",
"if": "Edit(*.ts)",
"timeout": 15,
"statusMessage": "Formatting with Biome..."
},
{
"type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/biome-format.sh",
"if": "Edit(*.tsx)",
"timeout": 15,
"statusMessage": "Formatting with Biome..."
},
{
"type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/biome-format.sh",
"if": "Edit(*.js)",
"timeout": 15,
"statusMessage": "Formatting with Biome..."
},
{
"type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/biome-format.sh",
"if": "Edit(*.jsx)",
"timeout": 15,
"statusMessage": "Formatting with Biome..."
},
{
"type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/biome-format.sh",
"if": "Edit(*.mjs)",
"timeout": 15,
"statusMessage": "Formatting with Biome..."
},
{
"type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/biome-format.sh",
"if": "Edit(*.cjs)",
"timeout": 15,
"statusMessage": "Formatting with Biome..."
},
{
"type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/biome-format.sh",
"if": "Edit(*.mts)",
"timeout": 15,
"statusMessage": "Formatting with Biome..."
},
{
"type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/biome-format.sh",
"if": "Edit(*.cts)",
"timeout": 15,
"statusMessage": "Formatting with Biome..."
},
{
"type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/biome-format.sh",
"if": "Edit(*.json)",
"timeout": 15,
"statusMessage": "Formatting with Biome..."
},
{
"type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/biome-format.sh",
"if": "Edit(*.jsonc)",
"timeout": 15,
"statusMessage": "Formatting with Biome..."
}
Expand Down
2 changes: 1 addition & 1 deletion plugins/disk-hygiene/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "disk-hygiene",
"version": "0.21.3",
"version": "0.21.4",
"description": "Context-aware disk hygiene for arbitrary directory trees: inventories orphaned and temporary artifacts, classifies evidence into review tiers, and offers exact-path cleanup only after a fresh safety preview and explicit per-tier approval. The target is read-only by default; OS-managed paths, links and mount points, VCS-tracked content without the complete checkout evidence bundle, changed entries, and live-handle uncertainty fail closed.",
"author": {
"name": "Melodic Software",
Expand Down
16 changes: 16 additions & 0 deletions plugins/disk-hygiene/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,22 @@
All notable changes to the `disk-hygiene` plugin are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning.

## [0.21.4]

### Changed

- **The engine gate carries an `if` filter, `Bash(*hygiene.py*)`.** The gate only ever
judges a command that carries the engine's file name (`_engine_gate_relevant`), so the
filter is a superset of its own relevance check; every other Bash call no longer pays
a Python interpreter start to be told it is irrelevant.
- **The engine gate is registered once per tool.** An `if` filter is scoped to the tool
it names, so the single `Bash|PowerShell` entry carrying a `Bash(...)` filter never
launched the gate for a PowerShell call. The `Bash` entry keeps the filter; a separate
`PowerShell` entry carries none, because PowerShell filtering must match every
subcommand of a compound command and would skip this kill-switch guard silently on a
mixed line. Every PowerShell call therefore still pays the interpreter start, as it
did before 0.21.4.

## [0.21.3]

### Changed
Expand Down
12 changes: 10 additions & 2 deletions plugins/disk-hygiene/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,15 @@ enforces nothing. The launcher resolves Python itself instead (#1504).

The guard registers on two surfaces: a plugin-level **engine gate** (`hooks/hooks.json`) that acts
only on commands referencing the engine, deferring everything else instantly, and enforces the kill
switch and data-root authority; and the skill-scoped **belt** inside the `clean` skill's context,
switch and data-root authority (since **0.21.4** the gate is registered once per tool: the `Bash`
entry carries the `if` filter `Bash(*hygiene.py*)`, a superset of the gate's own relevance check,
so a Bash command that does not name the engine no longer spawns the Python interpreter to be
deferred, except that a command containing `$()`, a backtick or `$VAR` still spawns it, because
the filter cannot see what the substitution expands to; the `PowerShell` entry carries no `if`,
because an `if` filter is scoped to the tool it names, so a Bash filter would leave every
PowerShell call unguarded, and a PowerShell filter must match every subcommand of a compound
command, which would skip this kill-switch guard silently on a mixed line, so every PowerShell
call still pays the interpreter start); and the skill-scoped **belt** inside the `clean` skill's context,
which adds the deny-by-default Bash and deletion-spelling PowerShell discipline during active
cleanup work. Both surfaces resolve the kill switch by reading `disk_hygiene_enabled` from
user-scope `pluginConfigs` in `settings.json` (located from `${CLAUDE_PLUGIN_ROOT}`, honored only
Expand All @@ -106,7 +114,7 @@ that issue. PreToolUse hooks also fire inside subagents, so fanned-out workers r
guards.

**A silent engine-gate launch or runtime failure is surfaced.** A `Stop`-event detector
(`skills/clean/scripts/guard_launch_monitor.py`, a second hook entry in `hooks/hooks.json`,
(`skills/clean/scripts/guard_launch_monitor.py`, a separate hook entry in `hooks/hooks.json`,
independent of the engine-gate guard itself) scans the session transcript for
`hook_non_blocking_error` records naming the engine gate's own command string and warns once per
session with the failure count and the most recent failure's exit code, duration, and stderr, so a
Expand Down
14 changes: 13 additions & 1 deletion plugins/disk-hygiene/hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,19 @@
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|PowerShell",
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/run-python-hook.sh \"${CLAUDE_PLUGIN_ROOT}\"/skills/clean/scripts/destructive_guard.py --mode engine-gate --plugin-root \"${CLAUDE_PLUGIN_ROOT}\" --authorized-data-root \"${CLAUDE_PLUGIN_DATA}\"",
"if": "Bash(*hygiene.py*)",
Comment thread
kyle-sexton marked this conversation as resolved.
Comment thread
kyle-sexton marked this conversation as resolved.
Comment thread
kyle-sexton marked this conversation as resolved.
Comment thread
kyle-sexton marked this conversation as resolved.
Comment thread
kyle-sexton marked this conversation as resolved.
Comment thread
kyle-sexton marked this conversation as resolved.
"shell": "bash",
"timeout": 60
}
]
},
{
"matcher": "PowerShell",
"hooks": [
{
"type": "command",
Expand Down
37 changes: 34 additions & 3 deletions plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py
Original file line number Diff line number Diff line change
Expand Up @@ -6352,8 +6352,37 @@ def _engine_gate_hook_args(cls) -> list[str]:
for hook in entry.get("hooks", [])
if any("destructive_guard.py" in token for token in cls._hook_argv(hook))
]
assert len(commands) == 1, commands
return cls._guard_argv_from_hook(commands[0], "destructive_guard.py")
# One registration per tool (``Bash`` with an ``if`` filter, ``PowerShell``
# without one), both carrying the same guard argv.
assert len(commands) == 2, commands
argvs = {
tuple(cls._guard_argv_from_hook(hook, "destructive_guard.py"))
for hook in commands
}
assert len(argvs) == 1, argvs
return list(argvs.pop())

def test_engine_gate_is_registered_once_per_tool(self) -> None:
"""Lock the per-tool registration shape of the plugin-level engine gate.

An ``if`` filter is scoped to the tool it names: under a single
``Bash|PowerShell`` matcher, ``Bash(...)`` filtered every PowerShell call
out of this kill-switch guard. The ``Bash`` entry keeps the filter; the
``PowerShell`` entry carries none, because a PowerShell filter must match
every subcommand of a compound command and would skip the guard silently
on a mixed line.
"""
hooks_path = SCRIPT_DIR.parents[2] / "hooks" / "hooks.json"
config = json.loads(hooks_path.read_text(encoding="utf-8"))
by_matcher = {
entry.get("matcher"): hook
for entry in config["hooks"]["PreToolUse"]
for hook in entry.get("hooks", [])
if any("destructive_guard.py" in token for token in self._hook_argv(hook))
}
self.assertEqual({"Bash", "PowerShell"}, set(by_matcher))
self.assertTrue(by_matcher["Bash"].get("if", "").startswith("Bash("))
self.assertNotIn("if", by_matcher["PowerShell"])

def test_engine_gate_hook_resolves_kill_switch_from_plugin_root_not_user_config(
self,
Expand Down Expand Up @@ -7149,7 +7178,9 @@ def test_declared_hook_timeouts_match_the_watchdog_ceiling(self) -> None:
for hook in entry.get("hooks", [])
if any("destructive_guard.py" in token for token in self._hook_argv(hook))
]
self.assertEqual([guard._DECLARED_HOOK_TIMEOUT_SECONDS], declared)
# One registration per tool, both declaring the same timeout.
self.assertEqual(2, len(declared), declared)
self.assertEqual({guard._DECLARED_HOOK_TIMEOUT_SECONDS}, set(declared))

skill_text = (SCRIPT_DIR.parent / "SKILL.md").read_text(encoding="utf-8")
timeout_lines = [
Expand Down
2 changes: 1 addition & 1 deletion plugins/go-format/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "go-format",
"version": "0.3.31",
"version": "0.3.32",
"description": "Auto-fix Go formatting and import management on edit via goimports \u2014 runs unconditionally (no consumer-config gate), skipping generated files.",
"author": {
"name": "Melodic Software",
Expand Down
8 changes: 8 additions & 0 deletions plugins/go-format/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
All notable changes to the `go-format` plugin are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning.

## [0.3.32]

### Changed

- **The hook carries an `if` filter, `Edit(*.go)`.** That is the one extension the hook
formats, so a Write/Edit of any other file no longer spawns it; behavior on Go files
is unchanged.

## [0.3.31]

### Changed
Expand Down
2 changes: 2 additions & 0 deletions plugins/go-format/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ surfaces the syntax error back to Claude as advisory context.
unconfigured. Running it does not impose a style choice a repo hasn't
made, the same reasoning that makes `gofmt` itself safe to run
unconditionally.
- **Spawned only for Go files.** The hook is registered with the `if` filter
`Edit(*.go)`, so a Write/Edit of any other file never starts a hook process for it.
- **Extension-scoped.** Only `.go` files trigger the hook (like
`ruff-format`'s `*.py`/`*.pyi` filter; unlike `typos-format`'s
language-agnostic scope).
Expand Down
1 change: 1 addition & 0 deletions plugins/go-format/hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
{
"type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/go-format.sh",
"if": "Edit(*.go)",
"timeout": 15,
"statusMessage": "Formatting Go imports..."
}
Expand Down
2 changes: 1 addition & 1 deletion plugins/guardrails/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,5 +147,5 @@
"min": 1
}
},
"version": "0.30.5"
"version": "0.31.0"
}
Loading
Loading