diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c22b59fdc..53d00b513 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -424,6 +424,22 @@ jobs: - name: Check plugin hook configs for bare userConfig argv tokens run: scripts/check-hook-userconfig-argv.sh + # Every option a plugin declares must be documented in that plugin's README. + # The block is generated from the manifest, so the failure this catches is an + # option added to plugin.json whose README block was never regenerated -- an + # undocumented knob is indistinguishable from one that does not exist, and the + # only place a user can discover it is the manifest itself. + plugin-options-docs-gate: + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - name: Check out + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Check plugin option docs are in sync with each manifest + run: python3 scripts/sync-plugin-options-docs.py --check + silent-skip-gate: runs-on: ubuntu-24.04 timeout-minutes: 15 @@ -1030,6 +1046,7 @@ jobs: - cross-plugin-source-drift - skill-leaf-name-gate - userconfig-argv-gate + - plugin-options-docs-gate - silent-skip-gate - plugin-manifest-presence-gate - orphaned-fixture-gate diff --git a/lib/hook-utils.sh b/lib/hook-utils.sh index dcb0e820f..e88315e72 100644 --- a/lib/hook-utils.sh +++ b/lib/hook-utils.sh @@ -16,11 +16,33 @@ readonly _HOOK_UTILS_LOADED=1 # read from the hook-process CLAUDE_PLUGIN_OPTION__ENABLED mirror. # Exits 0 (allow) if disabled. Place after source, before stdin parsing. # hook::check_enabled "MARKDOWN_FORMAT" # checks CLAUDE_PLUGIN_OPTION_MARKDOWN_FORMAT_ENABLED -hook::check_enabled() { +# +# Deliberately NOT layered with a marketplace-specific fleet switch. Claude Code +# already ships the coarse controls, and a parallel scheme here would become a +# second source of truth for the same question: +# * `--safe-mode` / `CLAUDE_CODE_SAFE_MODE` — start with every customization +# (CLAUDE.md, plugins, skills, hooks, MCP servers) disabled +# * `disableAllHooks` — disable all hooks and any custom status line +# * `claude plugin disable|enable ` — per-plugin, dependency-aware +# This helper stays scoped to the one thing it owns: the plugin's own +# `_enabled` userConfig boolean, surfaced to hook processes as the native +# `$CLAUDE_PLUGIN_OPTION_` mirror. + +# hook::is_enabled — the same check as a PREDICATE. Returns 0 when the +# plugin should run, 1 when it should not. For callers that must not terminate +# the process on a "disabled" answer. +# +# The statusline tee is exactly that caller: it is a TRANSPARENT WRAPPER around +# the user's real statusline, so exiting 0 on "disabled" would suppress the +# wrapped command's output and blank the status line. It needs to skip its own +# side effect and still pass through. +hook::is_enabled() { local var_name="CLAUDE_PLUGIN_OPTION_${1}_ENABLED" - if [[ "${!var_name:-true}" != "true" ]]; then - exit 0 - fi + [[ "${!var_name:-true}" == "true" ]] +} + +hook::check_enabled() { + hook::is_enabled "$1" || exit 0 } # --- Prerequisite visibility -------------------------------------------------- diff --git a/lib/hook-utils.test.sh b/lib/hook-utils.test.sh index 79865d1d8..619ec9e16 100755 --- a/lib/hook-utils.test.sh +++ b/lib/hook-utils.test.sh @@ -2295,6 +2295,82 @@ else fail "jq_fields early-return flag: expected 0, got '$HOOK_JQ_FIELDS_NUL'" fi +# --- Test 20: hook::is_enabled / hook::check_enabled -------------------------- +# `check_enabled` exits the process when a plugin is gated off, so a case cannot +# be asserted in-process: each runs in a child bash that sources the lib and +# prints a sentinel only if the gate let it through. Absence of the sentinel IS +# the "skipped" signal. Each probe unsets the control variable first so an +# ambient value in the test runner's own environment cannot mask a regression. +# +# Scope note: this gate reads ONLY the plugin's own userConfig mirror. Turning +# the whole fleet off is Claude Code's job (`--safe-mode`, `disableAllHooks`, +# `claude plugin disable`), not this library's — a second fleet-wide switch here +# would be a competing source of truth for the same question. +ce_probe() { + # ce_probe [VAR=VALUE ...] + local want="$1" desc="$2" + shift 2 + local got + got=$( + # shellcheck disable=SC2016 # $0 must NOT expand here: it is the child bash's + # own positional, bound to the library path passed after the -c string. + env -u CLAUDE_PLUGIN_OPTION_RATE_LIMIT_GUARD_ENABLED \ + "$@" bash -c 'source "$0"; hook::check_enabled "RATE_LIMIT_GUARD"; printf RUN' \ + "$HOOK_DIR/hook-utils.sh" 2>/dev/null + ) + got="${got:-SKIP}" + if [[ "$got" == "$want" ]]; then + ok "check_enabled: $desc" + else + fail "check_enabled: $desc — got '$got', want '$want'" + fi +} + +# The default must not move: every existing installation leaves this unset, so a +# regression here silently changes behavior for every user of the marketplace. +ce_probe RUN "unset stays enabled (backward compatibility)" +ce_probe RUN "explicit true" CLAUDE_PLUGIN_OPTION_RATE_LIMIT_GUARD_ENABLED=true +ce_probe SKIP "explicit false" CLAUDE_PLUGIN_OPTION_RATE_LIMIT_GUARD_ENABLED=false +# Anything that is not exactly "true" is off — a typo must not read as enabled. +ce_probe SKIP "a non-boolean value is not 'true'" CLAUDE_PLUGIN_OPTION_RATE_LIMIT_GUARD_ENABLED=yes +# An EMPTY value is treated as unset, not as false: `${var:-true}` falls back to +# the default. This is long-standing behavior, asserted here so a future change +# to the parameter expansion (`:-` to `-`) can't flip it silently. It matters +# because Claude Code exports every declared userConfig option to the hook +# environment, so an option the user never answered arrives as an empty string — +# and that must mean "default", not "disabled". +ce_probe RUN "empty value falls back to the default (treated as unset)" \ + CLAUDE_PLUGIN_OPTION_RATE_LIMIT_GUARD_ENABLED= + +# hook::is_enabled is the predicate form: same answer, but it RETURNS instead of +# exiting. The statusline tee depends on this — it wraps the user's real +# statusline, so an exit would blank the status line instead of just skipping +# the tee's own write. +ie_probe() { + local want="$1" desc="$2" + shift 2 + local got + got=$( + # shellcheck disable=SC2016 # $0 must NOT expand here: it is the child bash's + # own positional, bound to the library path passed after the -c string. + env -u CLAUDE_PLUGIN_OPTION_RATE_LIMIT_GUARD_ENABLED \ + "$@" bash -c 'source "$0" + if hook::is_enabled "RATE_LIMIT_GUARD"; then printf ENABLED; else printf DISABLED; fi + printf "+SURVIVED"' "$HOOK_DIR/hook-utils.sh" 2>/dev/null + ) + if [[ "$got" == "$want" ]]; then + ok "is_enabled: $desc" + else + fail "is_enabled: $desc — got '$got', want '$want'" + fi +} +ie_probe "ENABLED+SURVIVED" "unset returns true and does not exit" +ie_probe "ENABLED+SURVIVED" "explicit true returns true" \ + CLAUDE_PLUGIN_OPTION_RATE_LIMIT_GUARD_ENABLED=true +# The critical one: a disabled answer must NOT terminate the caller. +ie_probe "DISABLED+SURVIVED" "explicit false returns false WITHOUT exiting" \ + CLAUDE_PLUGIN_OPTION_RATE_LIMIT_GUARD_ENABLED=false + echo echo "PASS=$PASS FAIL=$FAIL" [[ $FAIL -eq 0 ]] diff --git a/plugins/actionlint/.claude-plugin/plugin.json b/plugins/actionlint/.claude-plugin/plugin.json index 6aabb755c..949cfdec2 100644 --- a/plugins/actionlint/.claude-plugin/plugin.json +++ b/plugins/actionlint/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "actionlint", - "version": "0.8.3", + "version": "0.8.4", "description": "Lint GitHub Actions workflow files on edit via actionlint, surfacing findings as advisory context.", "author": { "name": "Melodic Software", diff --git a/plugins/actionlint/CHANGELOG.md b/plugins/actionlint/CHANGELOG.md index eb8196ad4..5da000f02 100644 --- a/plugins/actionlint/CHANGELOG.md +++ b/plugins/actionlint/CHANGELOG.md @@ -3,6 +3,16 @@ 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.4] + +### Changed + +- **Carries the shared hook library's new `hook::is_enabled` predicate.** `hook::check_enabled` + exits the process when a plugin is gated off, which is correct for a hook but wrong for a + caller that must keep running afterward. The resolution is now also available as a predicate + that returns instead of exiting. No behaviour of this plugin changes; the version moves so + consumers receive the updated library. + ## [0.8.3] ### Fixed diff --git a/plugins/actionlint/README.md b/plugins/actionlint/README.md index 8af4b5d63..28a88a7af 100644 --- a/plugins/actionlint/README.md +++ b/plugins/actionlint/README.md @@ -79,6 +79,64 @@ here applies across every project. To disable actionlint for a single repository, disable the plugin in that project's `enabledPlugins` rather than setting `actionlint_enabled`. + + +### Options reference + +Generated from this plugin's `.claude-plugin/plugin.json`. Every option Claude Code +will prompt for when the plugin is enabled, with the environment variable each hook +reads it from. + +| Option | Type | Default | Environment variable | Description | +| --- | --- | --- | --- | --- | +| `actionlint_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_ACTIONLINT_ENABLED` | Lint GitHub Actions workflow files on edit via actionlint | +| `stdin_read_timeout` | number
*min 1* | `2` | `CLAUDE_PLUGIN_OPTION_STDIN_READ_TIMEOUT` | Idle bound on reading the hook payload from stdin — how long the pipe may go silent before the hook gives up and fails open | + +### How to set these + +Three supported routes, in the order most people want them: + +1. **Interactively** — Claude Code prompts for declared options when you enable the + plugin. To change them later: `/plugin configure actionlint`. +2. **Headless, at install time** — repeat `--config` for each option. Replace + `` with the marketplace you installed this plugin from: + + ```shell + claude plugin install actionlint@ --config actionlint_enabled= + ``` + +3. **By hand, in settings** — add the value under `pluginConfigs` in your **user** + settings (`~/.claude/settings.json`): + + ```json + { + "pluginConfigs": { + "actionlint@": { + "options": { + "actionlint_enabled": + } + } + } + } + ``` + + Plugin option values are read from **user**, `--settings`, and managed settings + only — **not** from a project's `.claude/settings.json`. To vary behavior per + repository, enable or disable the plugin in that project's `enabledPlugins` + instead of setting an option there. + +Do not set the `CLAUDE_PLUGIN_OPTION_*` variables yourself. They are how Claude Code +hands a configured value to a hook process; the value comes from the routes above. + +### Upstream documentation + +- [User configuration](https://code.claude.com/docs/en/plugins-reference#user-configuration) — the `userConfig` schema and the `CLAUDE_PLUGIN_OPTION_` export +- [Plugin settings](https://code.claude.com/docs/en/settings#plugin-settings) — `enabledPlugins`, `extraKnownMarketplaces`, `pluginConfigs` +- [Configuration scopes](https://code.claude.com/docs/en/settings#configuration-scopes) — user vs project vs local precedence +- [Manage installed plugins](https://code.claude.com/docs/en/discover-plugins#manage-installed-plugins) — enabling, disabling, `/plugin list` + + + ## License MIT (SPDX-License-Identifier: MIT). diff --git a/plugins/actionlint/hooks/hook-utils.sh b/plugins/actionlint/hooks/hook-utils.sh index dcb0e820f..e88315e72 100644 --- a/plugins/actionlint/hooks/hook-utils.sh +++ b/plugins/actionlint/hooks/hook-utils.sh @@ -16,11 +16,33 @@ readonly _HOOK_UTILS_LOADED=1 # read from the hook-process CLAUDE_PLUGIN_OPTION__ENABLED mirror. # Exits 0 (allow) if disabled. Place after source, before stdin parsing. # hook::check_enabled "MARKDOWN_FORMAT" # checks CLAUDE_PLUGIN_OPTION_MARKDOWN_FORMAT_ENABLED -hook::check_enabled() { +# +# Deliberately NOT layered with a marketplace-specific fleet switch. Claude Code +# already ships the coarse controls, and a parallel scheme here would become a +# second source of truth for the same question: +# * `--safe-mode` / `CLAUDE_CODE_SAFE_MODE` — start with every customization +# (CLAUDE.md, plugins, skills, hooks, MCP servers) disabled +# * `disableAllHooks` — disable all hooks and any custom status line +# * `claude plugin disable|enable ` — per-plugin, dependency-aware +# This helper stays scoped to the one thing it owns: the plugin's own +# `_enabled` userConfig boolean, surfaced to hook processes as the native +# `$CLAUDE_PLUGIN_OPTION_` mirror. + +# hook::is_enabled — the same check as a PREDICATE. Returns 0 when the +# plugin should run, 1 when it should not. For callers that must not terminate +# the process on a "disabled" answer. +# +# The statusline tee is exactly that caller: it is a TRANSPARENT WRAPPER around +# the user's real statusline, so exiting 0 on "disabled" would suppress the +# wrapped command's output and blank the status line. It needs to skip its own +# side effect and still pass through. +hook::is_enabled() { local var_name="CLAUDE_PLUGIN_OPTION_${1}_ENABLED" - if [[ "${!var_name:-true}" != "true" ]]; then - exit 0 - fi + [[ "${!var_name:-true}" == "true" ]] +} + +hook::check_enabled() { + hook::is_enabled "$1" || exit 0 } # --- Prerequisite visibility -------------------------------------------------- diff --git a/plugins/ai-briefing/README.md b/plugins/ai-briefing/README.md index f8183925a..21018241f 100644 --- a/plugins/ai-briefing/README.md +++ b/plugins/ai-briefing/README.md @@ -79,3 +79,60 @@ cannot escape that boundary. Machine-local state and generated artifacts live under `${CLAUDE_PLUGIN_DATA}`, keyed by profile. Tracked source, audience, and brand configuration always stays in the consumer repository. + + + +### Options reference + +Generated from this plugin's `.claude-plugin/plugin.json`. Every option Claude Code +will prompt for when the plugin is enabled, with the environment variable each hook +reads it from. + +| Option | Type | Default | Environment variable | Description | +| --- | --- | --- | --- | --- | +| `active_profile` | string | *(none)* | `CLAUDE_PLUGIN_OPTION_ACTIVE_PROFILE` | Portable 1-63 character lowercase-kebab name of the ai-briefing profile to use; reserved Windows device names are not allowed. Leave unset when there is a single profile (or only the default). A per-invocation --profile argument overrides this. | + +### How to set these + +Three supported routes, in the order most people want them: + +1. **Interactively** — Claude Code prompts for declared options when you enable the + plugin. To change them later: `/plugin configure ai-briefing`. +2. **Headless, at install time** — repeat `--config` for each option. Replace + `` with the marketplace you installed this plugin from: + + ```shell + claude plugin install ai-briefing@ --config active_profile= + ``` + +3. **By hand, in settings** — add the value under `pluginConfigs` in your **user** + settings (`~/.claude/settings.json`): + + ```json + { + "pluginConfigs": { + "ai-briefing@": { + "options": { + "active_profile": + } + } + } + } + ``` + + Plugin option values are read from **user**, `--settings`, and managed settings + only — **not** from a project's `.claude/settings.json`. To vary behavior per + repository, enable or disable the plugin in that project's `enabledPlugins` + instead of setting an option there. + +Do not set the `CLAUDE_PLUGIN_OPTION_*` variables yourself. They are how Claude Code +hands a configured value to a hook process; the value comes from the routes above. + +### Upstream documentation + +- [User configuration](https://code.claude.com/docs/en/plugins-reference#user-configuration) — the `userConfig` schema and the `CLAUDE_PLUGIN_OPTION_` export +- [Plugin settings](https://code.claude.com/docs/en/settings#plugin-settings) — `enabledPlugins`, `extraKnownMarketplaces`, `pluginConfigs` +- [Configuration scopes](https://code.claude.com/docs/en/settings#configuration-scopes) — user vs project vs local precedence +- [Manage installed plugins](https://code.claude.com/docs/en/discover-plugins#manage-installed-plugins) — enabling, disabling, `/plugin list` + + diff --git a/plugins/autonomy/.claude-plugin/plugin.json b/plugins/autonomy/.claude-plugin/plugin.json index bce46582e..ffcee745b 100644 --- a/plugins/autonomy/.claude-plugin/plugin.json +++ b/plugins/autonomy/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "autonomy", - "version": "0.14.3", + "version": "0.14.4", "description": "Governed autonomous agent operation: role-topology, binding-seam, wiring-vs-advisor, telemetry, return-accounting, trigger-dispatch, per-work-class guardrail-matrix, standing-routine-catalog, and design-only runner-charter contracts for climbing the AI-adoption ladder, plus a guided-setup skill that discovers an adopting org's state, writes its schema-versioned binding, wires standards-pinned OTLP emission with a zero-cost file-artifact default, wires human-attested return capture at the task boundary, wires signal adapters with one governed dispatch entrypoint, binds the five-class guardrail matrix to an org's isolation substrates with an in-boundary live-validation probe before recording each fail-closed binding, and stands up standing-routine-catalog classes as scheduled temporal signal adapters behind the one governed queue with free scheduling defaults wired as reviewable changes and each routine's work-class mapping homed on the security surface.", "author": { "name": "Melodic Software", diff --git a/plugins/autonomy/CHANGELOG.md b/plugins/autonomy/CHANGELOG.md index 8becb4e81..e300e9574 100644 --- a/plugins/autonomy/CHANGELOG.md +++ b/plugins/autonomy/CHANGELOG.md @@ -6,6 +6,16 @@ All notable changes to the `autonomy` plugin are documented here. Format follows Versions 0.1.0–0.7.0 predate this file (introduced with 0.7.1); their history lives in the merged work-package PRs (#333, #343, #356, #372, #377, #600, #676). +## [0.14.4] + +### Changed + +- **Carries the shared hook library's new `hook::is_enabled` predicate.** `hook::check_enabled` + exits the process when a plugin is gated off, which is correct for a hook but wrong for a + caller that must keep running afterward. The resolution is now also available as a predicate + that returns instead of exiting. No behaviour of this plugin changes; the version moves so + consumers receive the updated library. + ## [0.14.3] ### Fixed diff --git a/plugins/autonomy/README.md b/plugins/autonomy/README.md index f4e6b4b37..b619ab312 100644 --- a/plugins/autonomy/README.md +++ b/plugins/autonomy/README.md @@ -174,3 +174,66 @@ config outlives any plugin restructure). Personal overlays follow the marketplac convention: `.claude/autonomy/**/*.local.*` stays gitignored; layers resolve per the binding-seam ladder — user-global → org binding (when pointed) → project → local overlay — additively. + + + +### Options reference + +Generated from this plugin's `.claude-plugin/plugin.json`. Every option Claude Code +will prompt for when the plugin is enabled, with the environment variable each hook +reads it from. + +| Option | Type | Default | Environment variable | Description | +| --- | --- | --- | --- | --- | +| `lane_stop_gate_enabled` | boolean | `false` | `CLAUDE_PLUGIN_OPTION_LANE_STOP_GATE_ENABLED` | Opt an autonomous lane into the deterministic Stop-hook completion gate. Default OFF — a Stop-blocking hook must never engage for an interactive session. Honored from user or managed settings only (the gate reads those files itself); per-session lanes are armed by the claude-ops lane launcher instead. The env mirror is never authority (#1784). | +| `lane_stop_gate_sentinel` | string | `"LANE-STOP-OK"` | `CLAUDE_PLUGIN_OPTION_LANE_STOP_GATE_SENTINEL` | The exact token the agent emits in its final message to declare the lane's goal met and authorize a stop. Matched only when alone on its own line. Honored from user/managed settings or the launcher's arm record, never the bare environment. | +| `lane_stop_gate_marker` | string | *(none)* | `CLAUDE_PLUGIN_OPTION_LANE_STOP_GATE_MARKER` | Optional path to a completion-marker file whose existence also authorizes a stop (absolute, or relative to the session cwd). Empty disables the file signal. Honored from user/managed settings or the launcher's arm record, never the bare environment. | +| `lane_stop_gate_arm_id` | string | *(none)* | `CLAUDE_PLUGIN_OPTION_LANE_STOP_GATE_ARM_ID` | Written by the lane launcher at launch: names this session's arm record in the plugin's own data directory (hooks/lane-stop-gate-arm.sh). A capability pointer, never authority by itself — the gate validates it, honors only a record in its install-derived store, and binds it to the first presenting session. Not set by hand. | +| `lane_notify_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_LANE_NOTIFY_ENABLED` | Master switch for the operator alert fired when a lane stops without signaling completion. | +| `lane_notify_os_toast_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_LANE_NOTIFY_OS_TOAST_ENABLED` | OS-native desktop toast (macOS/Linux) for the lane-stop alert. | +| `lane_notify_terminal_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_LANE_NOTIFY_TERMINAL_ENABLED` | Audible bell + OSC 9 notification written to the controlling terminal for the lane-stop alert. | + +### How to set these + +Three supported routes, in the order most people want them: + +1. **Interactively** — Claude Code prompts for declared options when you enable the + plugin. To change them later: `/plugin configure autonomy`. +2. **Headless, at install time** — repeat `--config` for each option. Replace + `` with the marketplace you installed this plugin from: + + ```shell + claude plugin install autonomy@ --config lane_stop_gate_enabled= + ``` + +3. **By hand, in settings** — add the value under `pluginConfigs` in your **user** + settings (`~/.claude/settings.json`): + + ```json + { + "pluginConfigs": { + "autonomy@": { + "options": { + "lane_stop_gate_enabled": + } + } + } + } + ``` + + Plugin option values are read from **user**, `--settings`, and managed settings + only — **not** from a project's `.claude/settings.json`. To vary behavior per + repository, enable or disable the plugin in that project's `enabledPlugins` + instead of setting an option there. + +Do not set the `CLAUDE_PLUGIN_OPTION_*` variables yourself. They are how Claude Code +hands a configured value to a hook process; the value comes from the routes above. + +### Upstream documentation + +- [User configuration](https://code.claude.com/docs/en/plugins-reference#user-configuration) — the `userConfig` schema and the `CLAUDE_PLUGIN_OPTION_` export +- [Plugin settings](https://code.claude.com/docs/en/settings#plugin-settings) — `enabledPlugins`, `extraKnownMarketplaces`, `pluginConfigs` +- [Configuration scopes](https://code.claude.com/docs/en/settings#configuration-scopes) — user vs project vs local precedence +- [Manage installed plugins](https://code.claude.com/docs/en/discover-plugins#manage-installed-plugins) — enabling, disabling, `/plugin list` + + diff --git a/plugins/autonomy/hooks/hook-utils.sh b/plugins/autonomy/hooks/hook-utils.sh index dcb0e820f..e88315e72 100644 --- a/plugins/autonomy/hooks/hook-utils.sh +++ b/plugins/autonomy/hooks/hook-utils.sh @@ -16,11 +16,33 @@ readonly _HOOK_UTILS_LOADED=1 # read from the hook-process CLAUDE_PLUGIN_OPTION__ENABLED mirror. # Exits 0 (allow) if disabled. Place after source, before stdin parsing. # hook::check_enabled "MARKDOWN_FORMAT" # checks CLAUDE_PLUGIN_OPTION_MARKDOWN_FORMAT_ENABLED -hook::check_enabled() { +# +# Deliberately NOT layered with a marketplace-specific fleet switch. Claude Code +# already ships the coarse controls, and a parallel scheme here would become a +# second source of truth for the same question: +# * `--safe-mode` / `CLAUDE_CODE_SAFE_MODE` — start with every customization +# (CLAUDE.md, plugins, skills, hooks, MCP servers) disabled +# * `disableAllHooks` — disable all hooks and any custom status line +# * `claude plugin disable|enable ` — per-plugin, dependency-aware +# This helper stays scoped to the one thing it owns: the plugin's own +# `_enabled` userConfig boolean, surfaced to hook processes as the native +# `$CLAUDE_PLUGIN_OPTION_` mirror. + +# hook::is_enabled — the same check as a PREDICATE. Returns 0 when the +# plugin should run, 1 when it should not. For callers that must not terminate +# the process on a "disabled" answer. +# +# The statusline tee is exactly that caller: it is a TRANSPARENT WRAPPER around +# the user's real statusline, so exiting 0 on "disabled" would suppress the +# wrapped command's output and blank the status line. It needs to skip its own +# side effect and still pass through. +hook::is_enabled() { local var_name="CLAUDE_PLUGIN_OPTION_${1}_ENABLED" - if [[ "${!var_name:-true}" != "true" ]]; then - exit 0 - fi + [[ "${!var_name:-true}" == "true" ]] +} + +hook::check_enabled() { + hook::is_enabled "$1" || exit 0 } # --- Prerequisite visibility -------------------------------------------------- diff --git a/plugins/bash-format/.claude-plugin/plugin.json b/plugins/bash-format/.claude-plugin/plugin.json index efc813aa4..56452cd48 100644 --- a/plugins/bash-format/.claude-plugin/plugin.json +++ b/plugins/bash-format/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "bash-format", - "version": "0.7.3", + "version": "0.7.4", "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", diff --git a/plugins/bash-format/CHANGELOG.md b/plugins/bash-format/CHANGELOG.md index f15ed048c..ef40bf15e 100644 --- a/plugins/bash-format/CHANGELOG.md +++ b/plugins/bash-format/CHANGELOG.md @@ -3,6 +3,16 @@ 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.4] + +### Changed + +- **Carries the shared hook library's new `hook::is_enabled` predicate.** `hook::check_enabled` + exits the process when a plugin is gated off, which is correct for a hook but wrong for a + caller that must keep running afterward. The resolution is now also available as a predicate + that returns instead of exiting. No behaviour of this plugin changes; the version moves so + consumers receive the updated library. + ## [0.7.3] ### Fixed diff --git a/plugins/bash-format/README.md b/plugins/bash-format/README.md index c78cfcfda..14c5d9cc2 100644 --- a/plugins/bash-format/README.md +++ b/plugins/bash-format/README.md @@ -101,6 +101,63 @@ These options are user-scoped (stored in your user settings, not the project's). To turn the hook off for a single repository, disable the whole plugin in that project's `enabledPlugins` instead. + + +### Options reference + +Generated from this plugin's `.claude-plugin/plugin.json`. Every option Claude Code +will prompt for when the plugin is enabled, with the environment variable each hook +reads it from. + +| Option | Type | Default | Environment variable | Description | +| --- | --- | --- | --- | --- | +| `bash_format_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_BASH_FORMAT_ENABLED` | Lint and format shell scripts on edit via ShellCheck + shfmt | + +### How to set these + +Three supported routes, in the order most people want them: + +1. **Interactively** — Claude Code prompts for declared options when you enable the + plugin. To change them later: `/plugin configure bash-format`. +2. **Headless, at install time** — repeat `--config` for each option. Replace + `` with the marketplace you installed this plugin from: + + ```shell + claude plugin install bash-format@ --config bash_format_enabled= + ``` + +3. **By hand, in settings** — add the value under `pluginConfigs` in your **user** + settings (`~/.claude/settings.json`): + + ```json + { + "pluginConfigs": { + "bash-format@": { + "options": { + "bash_format_enabled": + } + } + } + } + ``` + + Plugin option values are read from **user**, `--settings`, and managed settings + only — **not** from a project's `.claude/settings.json`. To vary behavior per + repository, enable or disable the plugin in that project's `enabledPlugins` + instead of setting an option there. + +Do not set the `CLAUDE_PLUGIN_OPTION_*` variables yourself. They are how Claude Code +hands a configured value to a hook process; the value comes from the routes above. + +### Upstream documentation + +- [User configuration](https://code.claude.com/docs/en/plugins-reference#user-configuration) — the `userConfig` schema and the `CLAUDE_PLUGIN_OPTION_` export +- [Plugin settings](https://code.claude.com/docs/en/settings#plugin-settings) — `enabledPlugins`, `extraKnownMarketplaces`, `pluginConfigs` +- [Configuration scopes](https://code.claude.com/docs/en/settings#configuration-scopes) — user vs project vs local precedence +- [Manage installed plugins](https://code.claude.com/docs/en/discover-plugins#manage-installed-plugins) — enabling, disabling, `/plugin list` + + + ## License MIT (SPDX-License-Identifier: MIT). diff --git a/plugins/bash-format/hooks/hook-utils.sh b/plugins/bash-format/hooks/hook-utils.sh index dcb0e820f..e88315e72 100644 --- a/plugins/bash-format/hooks/hook-utils.sh +++ b/plugins/bash-format/hooks/hook-utils.sh @@ -16,11 +16,33 @@ readonly _HOOK_UTILS_LOADED=1 # read from the hook-process CLAUDE_PLUGIN_OPTION__ENABLED mirror. # Exits 0 (allow) if disabled. Place after source, before stdin parsing. # hook::check_enabled "MARKDOWN_FORMAT" # checks CLAUDE_PLUGIN_OPTION_MARKDOWN_FORMAT_ENABLED -hook::check_enabled() { +# +# Deliberately NOT layered with a marketplace-specific fleet switch. Claude Code +# already ships the coarse controls, and a parallel scheme here would become a +# second source of truth for the same question: +# * `--safe-mode` / `CLAUDE_CODE_SAFE_MODE` — start with every customization +# (CLAUDE.md, plugins, skills, hooks, MCP servers) disabled +# * `disableAllHooks` — disable all hooks and any custom status line +# * `claude plugin disable|enable ` — per-plugin, dependency-aware +# This helper stays scoped to the one thing it owns: the plugin's own +# `_enabled` userConfig boolean, surfaced to hook processes as the native +# `$CLAUDE_PLUGIN_OPTION_` mirror. + +# hook::is_enabled — the same check as a PREDICATE. Returns 0 when the +# plugin should run, 1 when it should not. For callers that must not terminate +# the process on a "disabled" answer. +# +# The statusline tee is exactly that caller: it is a TRANSPARENT WRAPPER around +# the user's real statusline, so exiting 0 on "disabled" would suppress the +# wrapped command's output and blank the status line. It needs to skip its own +# side effect and still pass through. +hook::is_enabled() { local var_name="CLAUDE_PLUGIN_OPTION_${1}_ENABLED" - if [[ "${!var_name:-true}" != "true" ]]; then - exit 0 - fi + [[ "${!var_name:-true}" == "true" ]] +} + +hook::check_enabled() { + hook::is_enabled "$1" || exit 0 } # --- Prerequisite visibility -------------------------------------------------- diff --git a/plugins/biome-format/.claude-plugin/plugin.json b/plugins/biome-format/.claude-plugin/plugin.json index 2afc5fa7b..0e56144f1 100644 --- a/plugins/biome-format/.claude-plugin/plugin.json +++ b/plugins/biome-format/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "biome-format", - "version": "0.6.3", + "version": "0.6.4", "description": "Auto-format and lint JS/TS/JSX/JSON on edit via Biome, only when a biome.json governs the repo — using the consuming repo's own Biome config.", "author": { "name": "Melodic Software", diff --git a/plugins/biome-format/CHANGELOG.md b/plugins/biome-format/CHANGELOG.md index c166cc4a7..b7525f86d 100644 --- a/plugins/biome-format/CHANGELOG.md +++ b/plugins/biome-format/CHANGELOG.md @@ -3,6 +3,16 @@ 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.4] + +### Changed + +- **Carries the shared hook library's new `hook::is_enabled` predicate.** `hook::check_enabled` + exits the process when a plugin is gated off, which is correct for a hook but wrong for a + caller that must keep running afterward. The resolution is now also available as a predicate + that returns instead of exiting. No behaviour of this plugin changes; the version moves so + consumers receive the updated library. + ## [0.6.3] ### Fixed diff --git a/plugins/biome-format/README.md b/plugins/biome-format/README.md index c17c304a9..8095931de 100644 --- a/plugins/biome-format/README.md +++ b/plugins/biome-format/README.md @@ -87,6 +87,63 @@ These options are user-scoped (stored in your user settings, not the project's). To turn the hook off for a single repository, disable the whole plugin in that project's `enabledPlugins` instead. + + +### Options reference + +Generated from this plugin's `.claude-plugin/plugin.json`. Every option Claude Code +will prompt for when the plugin is enabled, with the environment variable each hook +reads it from. + +| Option | Type | Default | Environment variable | Description | +| --- | --- | --- | --- | --- | +| `biome_format_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_BIOME_FORMAT_ENABLED` | Format and lint JS/TS/JSX/JSON on edit via Biome | + +### How to set these + +Three supported routes, in the order most people want them: + +1. **Interactively** — Claude Code prompts for declared options when you enable the + plugin. To change them later: `/plugin configure biome-format`. +2. **Headless, at install time** — repeat `--config` for each option. Replace + `` with the marketplace you installed this plugin from: + + ```shell + claude plugin install biome-format@ --config biome_format_enabled= + ``` + +3. **By hand, in settings** — add the value under `pluginConfigs` in your **user** + settings (`~/.claude/settings.json`): + + ```json + { + "pluginConfigs": { + "biome-format@": { + "options": { + "biome_format_enabled": + } + } + } + } + ``` + + Plugin option values are read from **user**, `--settings`, and managed settings + only — **not** from a project's `.claude/settings.json`. To vary behavior per + repository, enable or disable the plugin in that project's `enabledPlugins` + instead of setting an option there. + +Do not set the `CLAUDE_PLUGIN_OPTION_*` variables yourself. They are how Claude Code +hands a configured value to a hook process; the value comes from the routes above. + +### Upstream documentation + +- [User configuration](https://code.claude.com/docs/en/plugins-reference#user-configuration) — the `userConfig` schema and the `CLAUDE_PLUGIN_OPTION_` export +- [Plugin settings](https://code.claude.com/docs/en/settings#plugin-settings) — `enabledPlugins`, `extraKnownMarketplaces`, `pluginConfigs` +- [Configuration scopes](https://code.claude.com/docs/en/settings#configuration-scopes) — user vs project vs local precedence +- [Manage installed plugins](https://code.claude.com/docs/en/discover-plugins#manage-installed-plugins) — enabling, disabling, `/plugin list` + + + ## License MIT (SPDX-License-Identifier: MIT). diff --git a/plugins/biome-format/hooks/hook-utils.sh b/plugins/biome-format/hooks/hook-utils.sh index dcb0e820f..e88315e72 100644 --- a/plugins/biome-format/hooks/hook-utils.sh +++ b/plugins/biome-format/hooks/hook-utils.sh @@ -16,11 +16,33 @@ readonly _HOOK_UTILS_LOADED=1 # read from the hook-process CLAUDE_PLUGIN_OPTION__ENABLED mirror. # Exits 0 (allow) if disabled. Place after source, before stdin parsing. # hook::check_enabled "MARKDOWN_FORMAT" # checks CLAUDE_PLUGIN_OPTION_MARKDOWN_FORMAT_ENABLED -hook::check_enabled() { +# +# Deliberately NOT layered with a marketplace-specific fleet switch. Claude Code +# already ships the coarse controls, and a parallel scheme here would become a +# second source of truth for the same question: +# * `--safe-mode` / `CLAUDE_CODE_SAFE_MODE` — start with every customization +# (CLAUDE.md, plugins, skills, hooks, MCP servers) disabled +# * `disableAllHooks` — disable all hooks and any custom status line +# * `claude plugin disable|enable ` — per-plugin, dependency-aware +# This helper stays scoped to the one thing it owns: the plugin's own +# `_enabled` userConfig boolean, surfaced to hook processes as the native +# `$CLAUDE_PLUGIN_OPTION_` mirror. + +# hook::is_enabled — the same check as a PREDICATE. Returns 0 when the +# plugin should run, 1 when it should not. For callers that must not terminate +# the process on a "disabled" answer. +# +# The statusline tee is exactly that caller: it is a TRANSPARENT WRAPPER around +# the user's real statusline, so exiting 0 on "disabled" would suppress the +# wrapped command's output and blank the status line. It needs to skip its own +# side effect and still pass through. +hook::is_enabled() { local var_name="CLAUDE_PLUGIN_OPTION_${1}_ENABLED" - if [[ "${!var_name:-true}" != "true" ]]; then - exit 0 - fi + [[ "${!var_name:-true}" == "true" ]] +} + +hook::check_enabled() { + hook::is_enabled "$1" || exit 0 } # --- Prerequisite visibility -------------------------------------------------- diff --git a/plugins/bug-report/README.md b/plugins/bug-report/README.md index 6dabb914e..481761415 100644 --- a/plugins/bug-report/README.md +++ b/plugins/bug-report/README.md @@ -80,6 +80,63 @@ Otherwise the emitted report is the deliverable — copy it into your tracker. /plugin install bug-report@melodic-software ``` + + +### Options reference + +Generated from this plugin's `.claude-plugin/plugin.json`. Every option Claude Code +will prompt for when the plugin is enabled, with the environment variable each hook +reads it from. + +| Option | Type | Default | Environment variable | Description | +| --- | --- | --- | --- | --- | +| `output_dir` | directory | *(none)* | `CLAUDE_PLUGIN_OPTION_OUTPUT_DIR` | Where --file writes reports. When unset, reports go to the plugin's own persistent data directory. Set this to a path in your repository if you want bug reports committed alongside your code. | + +### How to set these + +Three supported routes, in the order most people want them: + +1. **Interactively** — Claude Code prompts for declared options when you enable the + plugin. To change them later: `/plugin configure bug-report`. +2. **Headless, at install time** — repeat `--config` for each option. Replace + `` with the marketplace you installed this plugin from: + + ```shell + claude plugin install bug-report@ --config output_dir= + ``` + +3. **By hand, in settings** — add the value under `pluginConfigs` in your **user** + settings (`~/.claude/settings.json`): + + ```json + { + "pluginConfigs": { + "bug-report@": { + "options": { + "output_dir": + } + } + } + } + ``` + + Plugin option values are read from **user**, `--settings`, and managed settings + only — **not** from a project's `.claude/settings.json`. To vary behavior per + repository, enable or disable the plugin in that project's `enabledPlugins` + instead of setting an option there. + +Do not set the `CLAUDE_PLUGIN_OPTION_*` variables yourself. They are how Claude Code +hands a configured value to a hook process; the value comes from the routes above. + +### Upstream documentation + +- [User configuration](https://code.claude.com/docs/en/plugins-reference#user-configuration) — the `userConfig` schema and the `CLAUDE_PLUGIN_OPTION_` export +- [Plugin settings](https://code.claude.com/docs/en/settings#plugin-settings) — `enabledPlugins`, `extraKnownMarketplaces`, `pluginConfigs` +- [Configuration scopes](https://code.claude.com/docs/en/settings#configuration-scopes) — user vs project vs local precedence +- [Manage installed plugins](https://code.claude.com/docs/en/discover-plugins#manage-installed-plugins) — enabling, disabling, `/plugin list` + + + ## License MIT (SPDX-License-Identifier: MIT). diff --git a/plugins/claude-ops/.claude-plugin/plugin.json b/plugins/claude-ops/.claude-plugin/plugin.json index 5c9958702..0d5b70319 100644 --- a/plugins/claude-ops/.claude-plugin/plugin.json +++ b/plugins/claude-ops/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "claude-ops", - "version": "0.28.5", + "version": "0.28.6", "description": "Claude Code operations toolkit. Seven skills: observability (read locally captured telemetry — OTEL store, collector, hook-event JSONL, ccusage — with trend reports and store pruning), known-issues (search known Claude product GitHub bugs, check service health, maintain a persistent tracked-issue registry), changelog (ingest Claude Code changelog entries and integrate them into the current repo), plugins (bring a machine's plugin fleet current on demand — marketplace refresh, effective-scope updates including in-repo project/local installs, new-plugin install per policy, scope-divergence detection and explicit convergence), morning-brief (read-only gh-based operator morning view — queue-label counts, merge-ready PRs, parked decisions with their RECOMMENDED lines, and loop-lane telemetry freshness), lanes (start/restart/stop/status loop lanes as named background Claude Code sessions seeded from canonical prompt files, with per-lane model/effort, a repo-pull + marketplace-refresh launch step, and a consume-restarts action — an OS-schedulable reader that relaunches stopped lanes whose telemetry carries a restart_request), and a re-runnable setup action that settles where the known-issues registry lives. Plus a family of seven advisory *-audit telemetry-emitter hooks (API errors, config changes, instruction loads, permission denials, pre-compaction, skill usage, tool failures) that emit the shared hook-telemetry envelope, and a reference sink that maps envelopes into the hook-events.jsonl the observability skill reads.", "author": { "name": "Melodic Software", diff --git a/plugins/claude-ops/CHANGELOG.md b/plugins/claude-ops/CHANGELOG.md index 5b70d4a95..866f9c535 100644 --- a/plugins/claude-ops/CHANGELOG.md +++ b/plugins/claude-ops/CHANGELOG.md @@ -3,6 +3,16 @@ All notable changes to the `claude-ops` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.28.6] + +### Changed + +- **Carries the shared hook library's new `hook::is_enabled` predicate.** `hook::check_enabled` + exits the process when a plugin is gated off, which is correct for a hook but wrong for a + caller that must keep running afterward. The resolution is now also available as a predicate + that returns instead of exiting. No behaviour of this plugin changes; the version moves so + consumers receive the updated library. + ## [0.28.5] ### Changed diff --git a/plugins/claude-ops/README.md b/plugins/claude-ops/README.md index 3b65fe961..e3650a4f7 100644 --- a/plugins/claude-ops/README.md +++ b/plugins/claude-ops/README.md @@ -237,6 +237,76 @@ project-relative defaults; the bundled scripts make no outbound network calls except `gh`/`curl` reads of GitHub and Claude status pages in the known-issues skill. + + +### Options reference + +Generated from this plugin's `.claude-plugin/plugin.json`. Every option Claude Code +will prompt for when the plugin is enabled, with the environment variable each hook +reads it from. + +| Option | Type | Default | Environment variable | Description | +| --- | --- | --- | --- | --- | +| `registry_dir` | string | *(none)* | `CLAUDE_PLUGIN_OPTION_REGISTRY_DIR` | Optional contained project-relative directory holding the known-issues registry (registry.json). Absolute, drive, UNC, traversal, and escaping-symlink paths are invalid. Leave unset to use ${CLAUDE_PLUGIN_DATA}. | +| `skill_usage_dir` | string | *(none)* | `CLAUDE_PLUGIN_OPTION_SKILL_USAGE_DIR` | Optional contained relative directory where the skill-usage-audit hooks write skill-usage.jsonl, resolved under the skill_usage_scope root (repo scope: the project root; user scope: $HOME). Absolute, drive, UNC, traversal, and escaping-symlink paths are invalid in every scope. Ignored by the data-dir scope (plugin-owned layout). Leave unset to use .claude/observability. | +| `skill_usage_scope` | string | `"repo"` | `CLAUDE_PLUGIN_OPTION_SKILL_USAGE_SCOPE` | Where the skill-usage store lives. Valid values: "repo" (default — project tree under the repo root, kept out of git status via a machine-local .git/info/exclude entry), "user" (the skill_usage_dir subpath under $HOME, one cross-repo store; rows carry a project field), "data-dir" (${CLAUDE_PLUGIN_DATA}/skill-usage/, plugin-owned and update-safe). The manifest schema has no enum type, so this validates in prose; any other value is treated as "repo" with a one-time advisory. | +| `skill_usage_git_exclude` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_SKILL_USAGE_GIT_EXCLUDE` | When the repo-scope store sits inside a git work tree, idempotently add its directory to .git/info/exclude (machine-local; never touches .gitignore or tracked files) so git status stays clean. Set false if your team deliberately commits the telemetry. | +| `install_new` | string | `"ask"` | `CLAUDE_PLUGIN_OPTION_INSTALL_NEW` | Controls what `sync` does with catalog plugins that aren't installed yet. Valid values: "ask" (default — offer them in one batched multi-select prompt), "all" (install every one automatically), "none" (report only, never install). The manifest schema has no enum type, so this validates in prose, not JSON Schema; any other value is treated as "ask". | +| `api_error_audit_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_API_ERROR_AUDIT_ENABLED` | Emit turn-failure telemetry on API errors | +| `config_change_audit_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_CONFIG_CHANGE_AUDIT_ENABLED` | Emit telemetry on config-source mutations | +| `instructions_loaded_audit_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_INSTRUCTIONS_LOADED_AUDIT_ENABLED` | Emit telemetry on rule/instruction file loads | +| `permission_denied_audit_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_PERMISSION_DENIED_AUDIT_ENABLED` | Emit telemetry on permission denials | +| `pre_compact_audit_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_PRE_COMPACT_AUDIT_ENABLED` | Emit telemetry on context-compaction events | +| `skill_usage_audit_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_SKILL_USAGE_AUDIT_ENABLED` | Emit telemetry on skill usage; shared by both skill-usage audit hooks (the Skill-tool and slash-command expansion paths) and also gates the shared skill-usage.jsonl store | +| `tool_failure_audit_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_TOOL_FAILURE_AUDIT_ENABLED` | Emit telemetry on Write/Edit/Bash tool failures | +| `instructions_loaded_audit_log_session_start` | boolean | `false` | `CLAUDE_PLUGIN_OPTION_INSTRUCTIONS_LOADED_AUDIT_LOG_SESSION_START` | Opt back into logging session_start instruction loads (dropped by default as deterministic and high-volume) | +| `stdin_read_timeout` | number
*min 1* | `2` | `CLAUDE_PLUGIN_OPTION_STDIN_READ_TIMEOUT` | Idle bound on reading the hook payload from stdin — how long the pipe may go silent before the hook gives up and fails open | + +### How to set these + +Three supported routes, in the order most people want them: + +1. **Interactively** — Claude Code prompts for declared options when you enable the + plugin. To change them later: `/plugin configure claude-ops`. +2. **Headless, at install time** — repeat `--config` for each option. Replace + `` with the marketplace you installed this plugin from: + + ```shell + claude plugin install claude-ops@ --config registry_dir= + ``` + +3. **By hand, in settings** — add the value under `pluginConfigs` in your **user** + settings (`~/.claude/settings.json`): + + ```json + { + "pluginConfigs": { + "claude-ops@": { + "options": { + "registry_dir": + } + } + } + } + ``` + + Plugin option values are read from **user**, `--settings`, and managed settings + only — **not** from a project's `.claude/settings.json`. To vary behavior per + repository, enable or disable the plugin in that project's `enabledPlugins` + instead of setting an option there. + +Do not set the `CLAUDE_PLUGIN_OPTION_*` variables yourself. They are how Claude Code +hands a configured value to a hook process; the value comes from the routes above. + +### Upstream documentation + +- [User configuration](https://code.claude.com/docs/en/plugins-reference#user-configuration) — the `userConfig` schema and the `CLAUDE_PLUGIN_OPTION_` export +- [Plugin settings](https://code.claude.com/docs/en/settings#plugin-settings) — `enabledPlugins`, `extraKnownMarketplaces`, `pluginConfigs` +- [Configuration scopes](https://code.claude.com/docs/en/settings#configuration-scopes) — user vs project vs local precedence +- [Manage installed plugins](https://code.claude.com/docs/en/discover-plugins#manage-installed-plugins) — enabling, disabling, `/plugin list` + + + ## License MIT (SPDX-License-Identifier: MIT). diff --git a/plugins/claude-ops/hooks/hook-utils.sh b/plugins/claude-ops/hooks/hook-utils.sh index dcb0e820f..e88315e72 100644 --- a/plugins/claude-ops/hooks/hook-utils.sh +++ b/plugins/claude-ops/hooks/hook-utils.sh @@ -16,11 +16,33 @@ readonly _HOOK_UTILS_LOADED=1 # read from the hook-process CLAUDE_PLUGIN_OPTION__ENABLED mirror. # Exits 0 (allow) if disabled. Place after source, before stdin parsing. # hook::check_enabled "MARKDOWN_FORMAT" # checks CLAUDE_PLUGIN_OPTION_MARKDOWN_FORMAT_ENABLED -hook::check_enabled() { +# +# Deliberately NOT layered with a marketplace-specific fleet switch. Claude Code +# already ships the coarse controls, and a parallel scheme here would become a +# second source of truth for the same question: +# * `--safe-mode` / `CLAUDE_CODE_SAFE_MODE` — start with every customization +# (CLAUDE.md, plugins, skills, hooks, MCP servers) disabled +# * `disableAllHooks` — disable all hooks and any custom status line +# * `claude plugin disable|enable ` — per-plugin, dependency-aware +# This helper stays scoped to the one thing it owns: the plugin's own +# `_enabled` userConfig boolean, surfaced to hook processes as the native +# `$CLAUDE_PLUGIN_OPTION_` mirror. + +# hook::is_enabled — the same check as a PREDICATE. Returns 0 when the +# plugin should run, 1 when it should not. For callers that must not terminate +# the process on a "disabled" answer. +# +# The statusline tee is exactly that caller: it is a TRANSPARENT WRAPPER around +# the user's real statusline, so exiting 0 on "disabled" would suppress the +# wrapped command's output and blank the status line. It needs to skip its own +# side effect and still pass through. +hook::is_enabled() { local var_name="CLAUDE_PLUGIN_OPTION_${1}_ENABLED" - if [[ "${!var_name:-true}" != "true" ]]; then - exit 0 - fi + [[ "${!var_name:-true}" == "true" ]] +} + +hook::check_enabled() { + hook::is_enabled "$1" || exit 0 } # --- Prerequisite visibility -------------------------------------------------- diff --git a/plugins/context-guard/.claude-plugin/plugin.json b/plugins/context-guard/.claude-plugin/plugin.json index 74d43cbdd..ea621b6a7 100644 --- a/plugins/context-guard/.claude-plugin/plugin.json +++ b/plugins/context-guard/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "context-guard", - "version": "0.6.4", + "version": "0.6.5", "description": "Per-session context-window observability plus the first shipped consumer: a statusline wrapper tees each session's context_window fields to a per-session snapshot file, a zone resolver classifies usage into smart/acceptable/dumb bands (percentage bands plus window-class token bands, conservative-min combination, zones.json SSOT with shipped defaults), a reader contract fixes how consuming sessions interpret the snapshots, and zone-crossing hooks report once per transition into a worse zone across two channels — the continuation menu to the operator, who owns that choice, and to the model only the zone determination plus the counter-steer that a zone word is not a decay signal (advisory by default; an optional blocking mode gates new mutating work on a fresh dumb-zone snapshot with handoff-writing exempt), with a PostCompact hook persisting an evidence-degraded marker.", "author": { "name": "Melodic Software", diff --git a/plugins/context-guard/CHANGELOG.md b/plugins/context-guard/CHANGELOG.md index 7f8bb7848..11522c20f 100644 --- a/plugins/context-guard/CHANGELOG.md +++ b/plugins/context-guard/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to the `context-guard` plugin. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.6.5] + +### Changed + +- **Carries the shared hook library's new `hook::is_enabled` predicate.** `hook::check_enabled` + exits the process when a plugin is gated off, which is correct for a hook but wrong for a + caller that must keep running afterward. The resolution is now also available as a predicate + that returns instead of exiting. No behaviour of this plugin changes; the version moves so + consumers receive the updated library. + ## [0.6.4] ### Fixed diff --git a/plugins/context-guard/README.md b/plugins/context-guard/README.md index 57735a976..a091c0cd0 100644 --- a/plugins/context-guard/README.md +++ b/plugins/context-guard/README.md @@ -122,6 +122,65 @@ The plugin's own zone-crossing hooks are the first shipped consumer. Next: the ` audit skill (zone-informed dispatch and evidence-flush decisions, conservative on `unknown`). Any session or tool on the machine may read the same files under the same contract. + + +### Options reference + +Generated from this plugin's `.claude-plugin/plugin.json`. Every option Claude Code +will prompt for when the plugin is enabled, with the environment variable each hook +reads it from. + +| Option | Type | Default | Environment variable | Description | +| --- | --- | --- | --- | --- | +| `context_guard_hooks_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_CONTEXT_GUARD_HOOKS_ENABLED` | Master switch for the zone-crossing injection, blocking gate, and PostCompact marker hooks | +| `zone_hook_mode` | string | `"advisory"` | `CLAUDE_PLUGIN_OPTION_ZONE_HOOK_MODE` | advisory (default) injects guidance only; blocking additionally denies new Write/Edit/NotebookEdit/Agent/Workflow calls on a fresh dumb-zone snapshot past the grace budget (fail-open on unknown; handoff-path writes, reads, Bash, and Skill stay allowed) | +| `zone_gate_grace_calls` | string | `"20"` | `CLAUDE_PLUGIN_OPTION_ZONE_GATE_GRACE_CALLS` | Blocking mode only: number of matched tool calls allowed after the session first resolves dumb before the gate denies (in-script default 20) | + +### How to set these + +Three supported routes, in the order most people want them: + +1. **Interactively** — Claude Code prompts for declared options when you enable the + plugin. To change them later: `/plugin configure context-guard`. +2. **Headless, at install time** — repeat `--config` for each option. Replace + `` with the marketplace you installed this plugin from: + + ```shell + claude plugin install context-guard@ --config context_guard_hooks_enabled= + ``` + +3. **By hand, in settings** — add the value under `pluginConfigs` in your **user** + settings (`~/.claude/settings.json`): + + ```json + { + "pluginConfigs": { + "context-guard@": { + "options": { + "context_guard_hooks_enabled": + } + } + } + } + ``` + + Plugin option values are read from **user**, `--settings`, and managed settings + only — **not** from a project's `.claude/settings.json`. To vary behavior per + repository, enable or disable the plugin in that project's `enabledPlugins` + instead of setting an option there. + +Do not set the `CLAUDE_PLUGIN_OPTION_*` variables yourself. They are how Claude Code +hands a configured value to a hook process; the value comes from the routes above. + +### Upstream documentation + +- [User configuration](https://code.claude.com/docs/en/plugins-reference#user-configuration) — the `userConfig` schema and the `CLAUDE_PLUGIN_OPTION_` export +- [Plugin settings](https://code.claude.com/docs/en/settings#plugin-settings) — `enabledPlugins`, `extraKnownMarketplaces`, `pluginConfigs` +- [Configuration scopes](https://code.claude.com/docs/en/settings#configuration-scopes) — user vs project vs local precedence +- [Manage installed plugins](https://code.claude.com/docs/en/discover-plugins#manage-installed-plugins) — enabling, disabling, `/plugin list` + + + ## License MIT (SPDX-License-Identifier: MIT). diff --git a/plugins/context-guard/hooks/hook-utils.sh b/plugins/context-guard/hooks/hook-utils.sh index dcb0e820f..e88315e72 100755 --- a/plugins/context-guard/hooks/hook-utils.sh +++ b/plugins/context-guard/hooks/hook-utils.sh @@ -16,11 +16,33 @@ readonly _HOOK_UTILS_LOADED=1 # read from the hook-process CLAUDE_PLUGIN_OPTION__ENABLED mirror. # Exits 0 (allow) if disabled. Place after source, before stdin parsing. # hook::check_enabled "MARKDOWN_FORMAT" # checks CLAUDE_PLUGIN_OPTION_MARKDOWN_FORMAT_ENABLED -hook::check_enabled() { +# +# Deliberately NOT layered with a marketplace-specific fleet switch. Claude Code +# already ships the coarse controls, and a parallel scheme here would become a +# second source of truth for the same question: +# * `--safe-mode` / `CLAUDE_CODE_SAFE_MODE` — start with every customization +# (CLAUDE.md, plugins, skills, hooks, MCP servers) disabled +# * `disableAllHooks` — disable all hooks and any custom status line +# * `claude plugin disable|enable ` — per-plugin, dependency-aware +# This helper stays scoped to the one thing it owns: the plugin's own +# `_enabled` userConfig boolean, surfaced to hook processes as the native +# `$CLAUDE_PLUGIN_OPTION_` mirror. + +# hook::is_enabled — the same check as a PREDICATE. Returns 0 when the +# plugin should run, 1 when it should not. For callers that must not terminate +# the process on a "disabled" answer. +# +# The statusline tee is exactly that caller: it is a TRANSPARENT WRAPPER around +# the user's real statusline, so exiting 0 on "disabled" would suppress the +# wrapped command's output and blank the status line. It needs to skip its own +# side effect and still pass through. +hook::is_enabled() { local var_name="CLAUDE_PLUGIN_OPTION_${1}_ENABLED" - if [[ "${!var_name:-true}" != "true" ]]; then - exit 0 - fi + [[ "${!var_name:-true}" == "true" ]] +} + +hook::check_enabled() { + hook::is_enabled "$1" || exit 0 } # --- Prerequisite visibility -------------------------------------------------- diff --git a/plugins/desktop-notification/.claude-plugin/plugin.json b/plugins/desktop-notification/.claude-plugin/plugin.json index e1c7ce944..83ba69b5f 100644 --- a/plugins/desktop-notification/.claude-plugin/plugin.json +++ b/plugins/desktop-notification/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "desktop-notification", - "version": "0.6.3", + "version": "0.6.4", "description": "Alert you when Claude Code needs input — an audible terminal bell, an OSC 9 terminal notification, and an OS-native toast (macOS/Linux) on permission and idle prompts.", "author": { "name": "Melodic Software", diff --git a/plugins/desktop-notification/CHANGELOG.md b/plugins/desktop-notification/CHANGELOG.md index 4fffff23a..f383eaee4 100644 --- a/plugins/desktop-notification/CHANGELOG.md +++ b/plugins/desktop-notification/CHANGELOG.md @@ -3,6 +3,16 @@ All notable changes to the `desktop-notification` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.6.4] + +### Changed + +- **Carries the shared hook library's new `hook::is_enabled` predicate.** `hook::check_enabled` + exits the process when a plugin is gated off, which is correct for a hook but wrong for a + caller that must keep running afterward. The resolution is now also available as a predicate + that returns instead of exiting. No behaviour of this plugin changes; the version moves so + consumers receive the updated library. + ## [0.6.3] ### Fixed diff --git a/plugins/desktop-notification/README.md b/plugins/desktop-notification/README.md index 5acd7e136..e9a02437d 100644 --- a/plugins/desktop-notification/README.md +++ b/plugins/desktop-notification/README.md @@ -90,6 +90,66 @@ When the consumer sets `HOOK_TELEMETRY_SINK` to an executable, the hook emits on `hook: "desktop-notification"`, `hook_event: "Notification"`, and a `data` payload of `notification_type` plus the `channels` that fired. Unset → exact no-op. + + +### Options reference + +Generated from this plugin's `.claude-plugin/plugin.json`. Every option Claude Code +will prompt for when the plugin is enabled, with the environment variable each hook +reads it from. + +| Option | Type | Default | Environment variable | Description | +| --- | --- | --- | --- | --- | +| `desktop_notification_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_DESKTOP_NOTIFICATION_ENABLED` | Master switch for the whole notification hook | +| `desktop_notification_bell_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_DESKTOP_NOTIFICATION_BELL_ENABLED` | Audible terminal bell (bare BEL) | +| `desktop_notification_terminal_notify_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_DESKTOP_NOTIFICATION_TERMINAL_NOTIFY_ENABLED` | OSC 9 terminal notification emitted via the hook's terminalSequence output | +| `desktop_notification_os_toast_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_DESKTOP_NOTIFICATION_OS_TOAST_ENABLED` | OS-native desktop toast (macOS/Linux) | + +### How to set these + +Three supported routes, in the order most people want them: + +1. **Interactively** — Claude Code prompts for declared options when you enable the + plugin. To change them later: `/plugin configure desktop-notification`. +2. **Headless, at install time** — repeat `--config` for each option. Replace + `` with the marketplace you installed this plugin from: + + ```shell + claude plugin install desktop-notification@ --config desktop_notification_enabled= + ``` + +3. **By hand, in settings** — add the value under `pluginConfigs` in your **user** + settings (`~/.claude/settings.json`): + + ```json + { + "pluginConfigs": { + "desktop-notification@": { + "options": { + "desktop_notification_enabled": + } + } + } + } + ``` + + Plugin option values are read from **user**, `--settings`, and managed settings + only — **not** from a project's `.claude/settings.json`. To vary behavior per + repository, enable or disable the plugin in that project's `enabledPlugins` + instead of setting an option there. + +Do not set the `CLAUDE_PLUGIN_OPTION_*` variables yourself. They are how Claude Code +hands a configured value to a hook process; the value comes from the routes above. + +### Upstream documentation + +- [User configuration](https://code.claude.com/docs/en/plugins-reference#user-configuration) — the `userConfig` schema and the `CLAUDE_PLUGIN_OPTION_` export +- [Plugin settings](https://code.claude.com/docs/en/settings#plugin-settings) — `enabledPlugins`, `extraKnownMarketplaces`, `pluginConfigs` +- [Configuration scopes](https://code.claude.com/docs/en/settings#configuration-scopes) — user vs project vs local precedence +- [Manage installed plugins](https://code.claude.com/docs/en/discover-plugins#manage-installed-plugins) — enabling, disabling, `/plugin list` + + + ## License MIT (SPDX-License-Identifier: MIT). diff --git a/plugins/desktop-notification/hooks/hook-utils.sh b/plugins/desktop-notification/hooks/hook-utils.sh index dcb0e820f..e88315e72 100644 --- a/plugins/desktop-notification/hooks/hook-utils.sh +++ b/plugins/desktop-notification/hooks/hook-utils.sh @@ -16,11 +16,33 @@ readonly _HOOK_UTILS_LOADED=1 # read from the hook-process CLAUDE_PLUGIN_OPTION__ENABLED mirror. # Exits 0 (allow) if disabled. Place after source, before stdin parsing. # hook::check_enabled "MARKDOWN_FORMAT" # checks CLAUDE_PLUGIN_OPTION_MARKDOWN_FORMAT_ENABLED -hook::check_enabled() { +# +# Deliberately NOT layered with a marketplace-specific fleet switch. Claude Code +# already ships the coarse controls, and a parallel scheme here would become a +# second source of truth for the same question: +# * `--safe-mode` / `CLAUDE_CODE_SAFE_MODE` — start with every customization +# (CLAUDE.md, plugins, skills, hooks, MCP servers) disabled +# * `disableAllHooks` — disable all hooks and any custom status line +# * `claude plugin disable|enable ` — per-plugin, dependency-aware +# This helper stays scoped to the one thing it owns: the plugin's own +# `_enabled` userConfig boolean, surfaced to hook processes as the native +# `$CLAUDE_PLUGIN_OPTION_` mirror. + +# hook::is_enabled — the same check as a PREDICATE. Returns 0 when the +# plugin should run, 1 when it should not. For callers that must not terminate +# the process on a "disabled" answer. +# +# The statusline tee is exactly that caller: it is a TRANSPARENT WRAPPER around +# the user's real statusline, so exiting 0 on "disabled" would suppress the +# wrapped command's output and blank the status line. It needs to skip its own +# side effect and still pass through. +hook::is_enabled() { local var_name="CLAUDE_PLUGIN_OPTION_${1}_ENABLED" - if [[ "${!var_name:-true}" != "true" ]]; then - exit 0 - fi + [[ "${!var_name:-true}" == "true" ]] +} + +hook::check_enabled() { + hook::is_enabled "$1" || exit 0 } # --- Prerequisite visibility -------------------------------------------------- diff --git a/plugins/discipline/README.md b/plugins/discipline/README.md index 0e47d74a3..0538f9016 100644 --- a/plugins/discipline/README.md +++ b/plugins/discipline/README.md @@ -388,3 +388,63 @@ read-only (it never writes config — reconfiguration stays the native flow). Batch membership and order otherwise live in each corrector's own colocated tier metadata (`metadata.discipline-batch` + `discipline-batch-rank`), so changing a shipped tier is a PR to that corrector. + + + +### Options reference + +Generated from this plugin's `.claude-plugin/plugin.json`. Every option Claude Code +will prompt for when the plugin is enabled, with the environment variable each hook +reads it from. + +| Option | Type | Default | Environment variable | Description | +| --- | --- | --- | --- | --- | +| `batch_exclude` | string | *(none)* | `CLAUDE_PLUGIN_OPTION_BATCH_EXCLUDE` | Comma-separated corrector skill names to drop from the posture batch (for example: point-dont-copy). Overrides the corrector's own declared tier. Empty runs the tiers exactly as the correctors declare them. | +| `batch_promote` | string | *(none)* | `CLAUDE_PLUGIN_OPTION_BATCH_PROMOTE` | Comma-separated situational corrector skill names to always run in the batch instead of gating them on relevance to the conversation. | +| `batch_demote` | string | *(none)* | `CLAUDE_PLUGIN_OPTION_BATCH_DEMOTE` | Comma-separated core corrector skill names to run only when relevant to the conversation instead of every session. | +| `research_deep_verification` | string | `"tiered"` | `CLAUDE_PLUGIN_OPTION_RESEARCH_DEEP_VERIFICATION` | Default verification depth for do-your-research-deep: 'tiered' (the default — resolve trivial and non-load-bearing inventory items inline, fan fresh-context subagents out only over the load-bearing ones) or 'full' (subagent-verify every inventory item). An invocation argument overrides this. An empty value, an unexpanded ${user_config.…} token, or an unrecognized string all fall back to tiered. | + +### How to set these + +Three supported routes, in the order most people want them: + +1. **Interactively** — Claude Code prompts for declared options when you enable the + plugin. To change them later: `/plugin configure discipline`. +2. **Headless, at install time** — repeat `--config` for each option. Replace + `` with the marketplace you installed this plugin from: + + ```shell + claude plugin install discipline@ --config batch_exclude= + ``` + +3. **By hand, in settings** — add the value under `pluginConfigs` in your **user** + settings (`~/.claude/settings.json`): + + ```json + { + "pluginConfigs": { + "discipline@": { + "options": { + "batch_exclude": + } + } + } + } + ``` + + Plugin option values are read from **user**, `--settings`, and managed settings + only — **not** from a project's `.claude/settings.json`. To vary behavior per + repository, enable or disable the plugin in that project's `enabledPlugins` + instead of setting an option there. + +Do not set the `CLAUDE_PLUGIN_OPTION_*` variables yourself. They are how Claude Code +hands a configured value to a hook process; the value comes from the routes above. + +### Upstream documentation + +- [User configuration](https://code.claude.com/docs/en/plugins-reference#user-configuration) — the `userConfig` schema and the `CLAUDE_PLUGIN_OPTION_` export +- [Plugin settings](https://code.claude.com/docs/en/settings#plugin-settings) — `enabledPlugins`, `extraKnownMarketplaces`, `pluginConfigs` +- [Configuration scopes](https://code.claude.com/docs/en/settings#configuration-scopes) — user vs project vs local precedence +- [Manage installed plugins](https://code.claude.com/docs/en/discover-plugins#manage-installed-plugins) — enabling, disabling, `/plugin list` + + diff --git a/plugins/disk-hygiene/README.md b/plugins/disk-hygiene/README.md index 4ff4b4326..6bf829327 100644 --- a/plugins/disk-hygiene/README.md +++ b/plugins/disk-hygiene/README.md @@ -250,6 +250,63 @@ Verified 2026-07-16 against current primary documentation: [Linux `unlink(2)`](https://man7.org/linux/man-pages/man2/unlink.2.html) — open-file unlink semantics motivate an explicit preflight rather than relying on deletion failure. + + +### Options reference + +Generated from this plugin's `.claude-plugin/plugin.json`. Every option Claude Code +will prompt for when the plugin is enabled, with the environment variable each hook +reads it from. + +| Option | Type | Default | Environment variable | Description | +| --- | --- | --- | --- | --- | +| `disk_hygiene_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_DISK_HYGIENE_ENABLED` | Allow the clean skill’s execution tiers; false = audit-only mode | + +### How to set these + +Three supported routes, in the order most people want them: + +1. **Interactively** — Claude Code prompts for declared options when you enable the + plugin. To change them later: `/plugin configure disk-hygiene`. +2. **Headless, at install time** — repeat `--config` for each option. Replace + `` with the marketplace you installed this plugin from: + + ```shell + claude plugin install disk-hygiene@ --config disk_hygiene_enabled= + ``` + +3. **By hand, in settings** — add the value under `pluginConfigs` in your **user** + settings (`~/.claude/settings.json`): + + ```json + { + "pluginConfigs": { + "disk-hygiene@": { + "options": { + "disk_hygiene_enabled": + } + } + } + } + ``` + + Plugin option values are read from **user**, `--settings`, and managed settings + only — **not** from a project's `.claude/settings.json`. To vary behavior per + repository, enable or disable the plugin in that project's `enabledPlugins` + instead of setting an option there. + +Do not set the `CLAUDE_PLUGIN_OPTION_*` variables yourself. They are how Claude Code +hands a configured value to a hook process; the value comes from the routes above. + +### Upstream documentation + +- [User configuration](https://code.claude.com/docs/en/plugins-reference#user-configuration) — the `userConfig` schema and the `CLAUDE_PLUGIN_OPTION_` export +- [Plugin settings](https://code.claude.com/docs/en/settings#plugin-settings) — `enabledPlugins`, `extraKnownMarketplaces`, `pluginConfigs` +- [Configuration scopes](https://code.claude.com/docs/en/settings#configuration-scopes) — user vs project vs local precedence +- [Manage installed plugins](https://code.claude.com/docs/en/discover-plugins#manage-installed-plugins) — enabling, disabling, `/plugin list` + + + ## License MIT (SPDX-License-Identifier: MIT). See the repository root `LICENSE`. diff --git a/plugins/dometrain/README.md b/plugins/dometrain/README.md index 60c6cb349..4a840649e 100644 --- a/plugins/dometrain/README.md +++ b/plugins/dometrain/README.md @@ -145,3 +145,60 @@ remains Dometrain's proprietary content, accessible under your Dometrain Pro sub This plugin ships no server code — the MCP server is Dometrain-hosted. There is no build step; `claude plugin validate plugins/dometrain` is the only local check. + + + +### Options reference + +Generated from this plugin's `.claude-plugin/plugin.json`. Every option Claude Code +will prompt for when the plugin is enabled, with the environment variable each hook +reads it from. + +| Option | Type | Default | Environment variable | Description | +| --- | --- | --- | --- | --- | +| `dometrain_api_key` | string
*required* | *(none)* | `CLAUDE_PLUGIN_OPTION_DOMETRAIN_API_KEY` | **Sensitive** — stored in the OS keychain or protected credentials file. Dometrain account API key from https://dometrain.com/dashboard/account/ (MCP API keys section). Required — the remote MCP server rejects requests without it. Stored by Claude Code in secure credential storage, never settings.json. | + +### How to set these + +Three supported routes, in the order most people want them: + +1. **Interactively** — Claude Code prompts for declared options when you enable the + plugin. To change them later: `/plugin configure dometrain`. +2. **Headless, at install time** — repeat `--config` for each option. Replace + `` with the marketplace you installed this plugin from: + + ```shell + claude plugin install dometrain@ --config dometrain_api_key= + ``` + +3. **By hand, in settings** — add the value under `pluginConfigs` in your **user** + settings (`~/.claude/settings.json`): + + ```json + { + "pluginConfigs": { + "dometrain@": { + "options": { + "dometrain_api_key": + } + } + } + } + ``` + + Plugin option values are read from **user**, `--settings`, and managed settings + only — **not** from a project's `.claude/settings.json`. To vary behavior per + repository, enable or disable the plugin in that project's `enabledPlugins` + instead of setting an option there. + +Do not set the `CLAUDE_PLUGIN_OPTION_*` variables yourself. They are how Claude Code +hands a configured value to a hook process; the value comes from the routes above. + +### Upstream documentation + +- [User configuration](https://code.claude.com/docs/en/plugins-reference#user-configuration) — the `userConfig` schema and the `CLAUDE_PLUGIN_OPTION_` export +- [Plugin settings](https://code.claude.com/docs/en/settings#plugin-settings) — `enabledPlugins`, `extraKnownMarketplaces`, `pluginConfigs` +- [Configuration scopes](https://code.claude.com/docs/en/settings#configuration-scopes) — user vs project vs local precedence +- [Manage installed plugins](https://code.claude.com/docs/en/discover-plugins#manage-installed-plugins) — enabling, disabling, `/plugin list` + + diff --git a/plugins/education/README.md b/plugins/education/README.md index c85870719..5919d593e 100644 --- a/plugins/education/README.md +++ b/plugins/education/README.md @@ -92,6 +92,64 @@ Configure them through the `/plugin` dialog, or headless at install time with non-home `report_library_dir` may be rejected by the hardcoded-path guardrails until the #798 path-indirection work lands. + + +### Options reference + +Generated from this plugin's `.claude-plugin/plugin.json`. Every option Claude Code +will prompt for when the plugin is enabled, with the environment variable each hook +reads it from. + +| Option | Type | Default | Environment variable | Description | +| --- | --- | --- | --- | --- | +| `quiz_policy` | string | `"on-request"` | `CLAUDE_PLUGIN_OPTION_QUIZ_POLICY` | When quiz-me offers a post-work comprehension quiz. One of: off (never offers), on-request (only when asked), always (after each completed change), above-threshold (when the change is large). Governs offer cadence only — a report is never generated without your confirmation. Unknown values are treated as on-request. | +| `report_library_dir` | directory | *(none)* | `CLAUDE_PLUGIN_OPTION_REPORT_LIBRARY_DIR` | Where quiz-me stores generated reports and quizzes. Unset uses the plugin's own persistent data directory; set it to a corpus checkout to redirect the library root there. Artifacts never land in the consuming repo's tree. | + +### How to set these + +Three supported routes, in the order most people want them: + +1. **Interactively** — Claude Code prompts for declared options when you enable the + plugin. To change them later: `/plugin configure education`. +2. **Headless, at install time** — repeat `--config` for each option. Replace + `` with the marketplace you installed this plugin from: + + ```shell + claude plugin install education@ --config quiz_policy= + ``` + +3. **By hand, in settings** — add the value under `pluginConfigs` in your **user** + settings (`~/.claude/settings.json`): + + ```json + { + "pluginConfigs": { + "education@": { + "options": { + "quiz_policy": + } + } + } + } + ``` + + Plugin option values are read from **user**, `--settings`, and managed settings + only — **not** from a project's `.claude/settings.json`. To vary behavior per + repository, enable or disable the plugin in that project's `enabledPlugins` + instead of setting an option there. + +Do not set the `CLAUDE_PLUGIN_OPTION_*` variables yourself. They are how Claude Code +hands a configured value to a hook process; the value comes from the routes above. + +### Upstream documentation + +- [User configuration](https://code.claude.com/docs/en/plugins-reference#user-configuration) — the `userConfig` schema and the `CLAUDE_PLUGIN_OPTION_` export +- [Plugin settings](https://code.claude.com/docs/en/settings#plugin-settings) — `enabledPlugins`, `extraKnownMarketplaces`, `pluginConfigs` +- [Configuration scopes](https://code.claude.com/docs/en/settings#configuration-scopes) — user vs project vs local precedence +- [Manage installed plugins](https://code.claude.com/docs/en/discover-plugins#manage-installed-plugins) — enabling, disabling, `/plugin list` + + + ## License MIT (SPDX-License-Identifier: MIT). diff --git a/plugins/eol-normalizer/.claude-plugin/plugin.json b/plugins/eol-normalizer/.claude-plugin/plugin.json index c6d5e890f..a61793460 100644 --- a/plugins/eol-normalizer/.claude-plugin/plugin.json +++ b/plugins/eol-normalizer/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "eol-normalizer", - "version": "0.6.3", + "version": "0.6.4", "description": "Normalize a written file's working-tree line endings to its .gitattributes eol value on edit — symmetric CRLF/LF driven by git check-attr, advisory and never blocking.", "author": { "name": "Melodic Software", diff --git a/plugins/eol-normalizer/CHANGELOG.md b/plugins/eol-normalizer/CHANGELOG.md index 1087db6e3..33eda1a75 100644 --- a/plugins/eol-normalizer/CHANGELOG.md +++ b/plugins/eol-normalizer/CHANGELOG.md @@ -3,6 +3,16 @@ All notable changes to the `eol-normalizer` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.6.4] + +### Changed + +- **Carries the shared hook library's new `hook::is_enabled` predicate.** `hook::check_enabled` + exits the process when a plugin is gated off, which is correct for a hook but wrong for a + caller that must keep running afterward. The resolution is now also available as a predicate + that returns instead of exiting. No behaviour of this plugin changes; the version moves so + consumers receive the updated library. + ## [0.6.3] ### Fixed diff --git a/plugins/eol-normalizer/README.md b/plugins/eol-normalizer/README.md index f82daa1e9..47511a59d 100644 --- a/plugins/eol-normalizer/README.md +++ b/plugins/eol-normalizer/README.md @@ -78,6 +78,63 @@ These options are user-scoped (stored in your user settings, not the project's). To turn the plugin off for a single repository, disable it in that project's `enabledPlugins` instead. + + +### Options reference + +Generated from this plugin's `.claude-plugin/plugin.json`. Every option Claude Code +will prompt for when the plugin is enabled, with the environment variable each hook +reads it from. + +| Option | Type | Default | Environment variable | Description | +| --- | --- | --- | --- | --- | +| `eol_normalizer_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_EOL_NORMALIZER_ENABLED` | Normalize a written file's line endings to its .gitattributes eol value | + +### How to set these + +Three supported routes, in the order most people want them: + +1. **Interactively** — Claude Code prompts for declared options when you enable the + plugin. To change them later: `/plugin configure eol-normalizer`. +2. **Headless, at install time** — repeat `--config` for each option. Replace + `` with the marketplace you installed this plugin from: + + ```shell + claude plugin install eol-normalizer@ --config eol_normalizer_enabled= + ``` + +3. **By hand, in settings** — add the value under `pluginConfigs` in your **user** + settings (`~/.claude/settings.json`): + + ```json + { + "pluginConfigs": { + "eol-normalizer@": { + "options": { + "eol_normalizer_enabled": + } + } + } + } + ``` + + Plugin option values are read from **user**, `--settings`, and managed settings + only — **not** from a project's `.claude/settings.json`. To vary behavior per + repository, enable or disable the plugin in that project's `enabledPlugins` + instead of setting an option there. + +Do not set the `CLAUDE_PLUGIN_OPTION_*` variables yourself. They are how Claude Code +hands a configured value to a hook process; the value comes from the routes above. + +### Upstream documentation + +- [User configuration](https://code.claude.com/docs/en/plugins-reference#user-configuration) — the `userConfig` schema and the `CLAUDE_PLUGIN_OPTION_` export +- [Plugin settings](https://code.claude.com/docs/en/settings#plugin-settings) — `enabledPlugins`, `extraKnownMarketplaces`, `pluginConfigs` +- [Configuration scopes](https://code.claude.com/docs/en/settings#configuration-scopes) — user vs project vs local precedence +- [Manage installed plugins](https://code.claude.com/docs/en/discover-plugins#manage-installed-plugins) — enabling, disabling, `/plugin list` + + + ## License MIT (SPDX-License-Identifier: MIT). diff --git a/plugins/eol-normalizer/hooks/hook-utils.sh b/plugins/eol-normalizer/hooks/hook-utils.sh index dcb0e820f..e88315e72 100644 --- a/plugins/eol-normalizer/hooks/hook-utils.sh +++ b/plugins/eol-normalizer/hooks/hook-utils.sh @@ -16,11 +16,33 @@ readonly _HOOK_UTILS_LOADED=1 # read from the hook-process CLAUDE_PLUGIN_OPTION__ENABLED mirror. # Exits 0 (allow) if disabled. Place after source, before stdin parsing. # hook::check_enabled "MARKDOWN_FORMAT" # checks CLAUDE_PLUGIN_OPTION_MARKDOWN_FORMAT_ENABLED -hook::check_enabled() { +# +# Deliberately NOT layered with a marketplace-specific fleet switch. Claude Code +# already ships the coarse controls, and a parallel scheme here would become a +# second source of truth for the same question: +# * `--safe-mode` / `CLAUDE_CODE_SAFE_MODE` — start with every customization +# (CLAUDE.md, plugins, skills, hooks, MCP servers) disabled +# * `disableAllHooks` — disable all hooks and any custom status line +# * `claude plugin disable|enable ` — per-plugin, dependency-aware +# This helper stays scoped to the one thing it owns: the plugin's own +# `_enabled` userConfig boolean, surfaced to hook processes as the native +# `$CLAUDE_PLUGIN_OPTION_` mirror. + +# hook::is_enabled — the same check as a PREDICATE. Returns 0 when the +# plugin should run, 1 when it should not. For callers that must not terminate +# the process on a "disabled" answer. +# +# The statusline tee is exactly that caller: it is a TRANSPARENT WRAPPER around +# the user's real statusline, so exiting 0 on "disabled" would suppress the +# wrapped command's output and blank the status line. It needs to skip its own +# side effect and still pass through. +hook::is_enabled() { local var_name="CLAUDE_PLUGIN_OPTION_${1}_ENABLED" - if [[ "${!var_name:-true}" != "true" ]]; then - exit 0 - fi + [[ "${!var_name:-true}" == "true" ]] +} + +hook::check_enabled() { + hook::is_enabled "$1" || exit 0 } # --- Prerequisite visibility -------------------------------------------------- diff --git a/plugins/github/README.md b/plugins/github/README.md index b99f55238..0d8032c71 100644 --- a/plugins/github/README.md +++ b/plugins/github/README.md @@ -76,3 +76,60 @@ From whichever marketplace distributes this plugin: /plugin marketplace add / /plugin install github@ ``` + + + +### Options reference + +Generated from this plugin's `.claude-plugin/plugin.json`. Every option Claude Code +will prompt for when the plugin is enabled, with the environment variable each hook +reads it from. + +| Option | Type | Default | Environment variable | Description | +| --- | --- | --- | --- | --- | +| `offer_browser_automation` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_OFFER_BROWSER_AUTOMATION` | When a settings surface is UI-only, offer opt-in browser automation (never auto-fired; every action individually confirmed). Set false to suppress the offer entirely. Advisory: honored by the skills' prose, layered under the per-action confirm. | + +### How to set these + +Three supported routes, in the order most people want them: + +1. **Interactively** — Claude Code prompts for declared options when you enable the + plugin. To change them later: `/plugin configure github`. +2. **Headless, at install time** — repeat `--config` for each option. Replace + `` with the marketplace you installed this plugin from: + + ```shell + claude plugin install github@ --config offer_browser_automation= + ``` + +3. **By hand, in settings** — add the value under `pluginConfigs` in your **user** + settings (`~/.claude/settings.json`): + + ```json + { + "pluginConfigs": { + "github@": { + "options": { + "offer_browser_automation": + } + } + } + } + ``` + + Plugin option values are read from **user**, `--settings`, and managed settings + only — **not** from a project's `.claude/settings.json`. To vary behavior per + repository, enable or disable the plugin in that project's `enabledPlugins` + instead of setting an option there. + +Do not set the `CLAUDE_PLUGIN_OPTION_*` variables yourself. They are how Claude Code +hands a configured value to a hook process; the value comes from the routes above. + +### Upstream documentation + +- [User configuration](https://code.claude.com/docs/en/plugins-reference#user-configuration) — the `userConfig` schema and the `CLAUDE_PLUGIN_OPTION_` export +- [Plugin settings](https://code.claude.com/docs/en/settings#plugin-settings) — `enabledPlugins`, `extraKnownMarketplaces`, `pluginConfigs` +- [Configuration scopes](https://code.claude.com/docs/en/settings#configuration-scopes) — user vs project vs local precedence +- [Manage installed plugins](https://code.claude.com/docs/en/discover-plugins#manage-installed-plugins) — enabling, disabling, `/plugin list` + + diff --git a/plugins/go-format/.claude-plugin/plugin.json b/plugins/go-format/.claude-plugin/plugin.json index 0aa7e35bc..3e1e592b6 100644 --- a/plugins/go-format/.claude-plugin/plugin.json +++ b/plugins/go-format/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "go-format", - "version": "0.3.3", + "version": "0.3.4", "description": "Auto-fix Go formatting and import management on edit via goimports — runs unconditionally (no consumer-config gate), skipping generated files.", "author": { "name": "Melodic Software", diff --git a/plugins/go-format/CHANGELOG.md b/plugins/go-format/CHANGELOG.md index 3fa4a2585..a68d65be8 100644 --- a/plugins/go-format/CHANGELOG.md +++ b/plugins/go-format/CHANGELOG.md @@ -3,6 +3,16 @@ 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.4] + +### Changed + +- **Carries the shared hook library's new `hook::is_enabled` predicate.** `hook::check_enabled` + exits the process when a plugin is gated off, which is correct for a hook but wrong for a + caller that must keep running afterward. The resolution is now also available as a predicate + that returns instead of exiting. No behaviour of this plugin changes; the version moves so + consumers receive the updated library. + ## [0.3.3] ### Fixed diff --git a/plugins/go-format/README.md b/plugins/go-format/README.md index 15c84d3f6..e5d076154 100644 --- a/plugins/go-format/README.md +++ b/plugins/go-format/README.md @@ -96,6 +96,63 @@ These options are user-scoped (stored in your user settings, not the project's). To turn the plugin off for a single repository, disable it in that project's `enabledPlugins` instead. + + +### Options reference + +Generated from this plugin's `.claude-plugin/plugin.json`. Every option Claude Code +will prompt for when the plugin is enabled, with the environment variable each hook +reads it from. + +| Option | Type | Default | Environment variable | Description | +| --- | --- | --- | --- | --- | +| `go_format_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_GO_FORMAT_ENABLED` | Run goimports -w on edit of a Go file | + +### How to set these + +Three supported routes, in the order most people want them: + +1. **Interactively** — Claude Code prompts for declared options when you enable the + plugin. To change them later: `/plugin configure go-format`. +2. **Headless, at install time** — repeat `--config` for each option. Replace + `` with the marketplace you installed this plugin from: + + ```shell + claude plugin install go-format@ --config go_format_enabled= + ``` + +3. **By hand, in settings** — add the value under `pluginConfigs` in your **user** + settings (`~/.claude/settings.json`): + + ```json + { + "pluginConfigs": { + "go-format@": { + "options": { + "go_format_enabled": + } + } + } + } + ``` + + Plugin option values are read from **user**, `--settings`, and managed settings + only — **not** from a project's `.claude/settings.json`. To vary behavior per + repository, enable or disable the plugin in that project's `enabledPlugins` + instead of setting an option there. + +Do not set the `CLAUDE_PLUGIN_OPTION_*` variables yourself. They are how Claude Code +hands a configured value to a hook process; the value comes from the routes above. + +### Upstream documentation + +- [User configuration](https://code.claude.com/docs/en/plugins-reference#user-configuration) — the `userConfig` schema and the `CLAUDE_PLUGIN_OPTION_` export +- [Plugin settings](https://code.claude.com/docs/en/settings#plugin-settings) — `enabledPlugins`, `extraKnownMarketplaces`, `pluginConfigs` +- [Configuration scopes](https://code.claude.com/docs/en/settings#configuration-scopes) — user vs project vs local precedence +- [Manage installed plugins](https://code.claude.com/docs/en/discover-plugins#manage-installed-plugins) — enabling, disabling, `/plugin list` + + + ## License MIT (SPDX-License-Identifier: MIT). diff --git a/plugins/go-format/hooks/hook-utils.sh b/plugins/go-format/hooks/hook-utils.sh index dcb0e820f..e88315e72 100644 --- a/plugins/go-format/hooks/hook-utils.sh +++ b/plugins/go-format/hooks/hook-utils.sh @@ -16,11 +16,33 @@ readonly _HOOK_UTILS_LOADED=1 # read from the hook-process CLAUDE_PLUGIN_OPTION__ENABLED mirror. # Exits 0 (allow) if disabled. Place after source, before stdin parsing. # hook::check_enabled "MARKDOWN_FORMAT" # checks CLAUDE_PLUGIN_OPTION_MARKDOWN_FORMAT_ENABLED -hook::check_enabled() { +# +# Deliberately NOT layered with a marketplace-specific fleet switch. Claude Code +# already ships the coarse controls, and a parallel scheme here would become a +# second source of truth for the same question: +# * `--safe-mode` / `CLAUDE_CODE_SAFE_MODE` — start with every customization +# (CLAUDE.md, plugins, skills, hooks, MCP servers) disabled +# * `disableAllHooks` — disable all hooks and any custom status line +# * `claude plugin disable|enable ` — per-plugin, dependency-aware +# This helper stays scoped to the one thing it owns: the plugin's own +# `_enabled` userConfig boolean, surfaced to hook processes as the native +# `$CLAUDE_PLUGIN_OPTION_` mirror. + +# hook::is_enabled — the same check as a PREDICATE. Returns 0 when the +# plugin should run, 1 when it should not. For callers that must not terminate +# the process on a "disabled" answer. +# +# The statusline tee is exactly that caller: it is a TRANSPARENT WRAPPER around +# the user's real statusline, so exiting 0 on "disabled" would suppress the +# wrapped command's output and blank the status line. It needs to skip its own +# side effect and still pass through. +hook::is_enabled() { local var_name="CLAUDE_PLUGIN_OPTION_${1}_ENABLED" - if [[ "${!var_name:-true}" != "true" ]]; then - exit 0 - fi + [[ "${!var_name:-true}" == "true" ]] +} + +hook::check_enabled() { + hook::is_enabled "$1" || exit 0 } # --- Prerequisite visibility -------------------------------------------------- diff --git a/plugins/guardrails/.claude-plugin/plugin.json b/plugins/guardrails/.claude-plugin/plugin.json index d0e755b89..400877dc2 100644 --- a/plugins/guardrails/.claude-plugin/plugin.json +++ b/plugins/guardrails/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "guardrails", - "version": "0.24.2", + "version": "0.24.3", "description": "Twelve safety guards that block secret/credential writes, hardcoded machine-specific paths, git hook-bypass attempts, irreversible git operations (force-push, reset --hard, worktree-wide checkout/restore discards), Bash file-write workarounds that circumvent Write/Edit hooks, multi-line `git commit -m` messages (an actual-newline `-m` mangles across shells; single-line `-m` passes), commit subjects and gh pr create titles that violate the repo's tracked team convention (when one is declared in .claude/source-control.md), (advisory) hallucinated CLI flags, (advisory) /plugin:skill references that do not resolve, (advisory) markdown citing a repo path the repo's own history shows was removed, (advisory, opt-in) un-throttled Workflow fan-out that risks burst 529s, and (advisory, opt-in) direct gh pr create calls bypassing this marketplace's own pull-request skill — each independently toggleable.", "author": { "name": "Melodic Software", diff --git a/plugins/guardrails/CHANGELOG.md b/plugins/guardrails/CHANGELOG.md index 0a55010b6..d3c200eb7 100644 --- a/plugins/guardrails/CHANGELOG.md +++ b/plugins/guardrails/CHANGELOG.md @@ -3,6 +3,16 @@ All notable changes to the `guardrails` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.24.3] + +### Changed + +- **Carries the shared hook library's new `hook::is_enabled` predicate.** `hook::check_enabled` + exits the process when a plugin is gated off, which is correct for a hook but wrong for a + caller that must keep running afterward. The resolution is now also available as a predicate + that returns instead of exiting. No behaviour of this plugin changes; the version moves so + consumers receive the updated library. + ## [0.24.2] ### Changed diff --git a/plugins/guardrails/README.md b/plugins/guardrails/README.md index 1e7f7996d..9af0a9e7f 100644 --- a/plugins/guardrails/README.md +++ b/plugins/guardrails/README.md @@ -252,6 +252,80 @@ Then verify the runtime prerequisites and live guard surface with `/guardrails:setup check`; `/guardrails:setup apply` resolves anything the check reports with guidance. + + +### Options reference + +Generated from this plugin's `.claude-plugin/plugin.json`. Every option Claude Code +will prompt for when the plugin is enabled, with the environment variable each hook +reads it from. + +| Option | Type | Default | Environment variable | Description | +| --- | --- | --- | --- | --- | +| `secret_pattern_detection_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_SECRET_PATTERN_DETECTION_ENABLED` | Block writes containing high-confidence secret/credential patterns | +| `hardcoded_path_check_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_HARDCODED_PATH_CHECK_ENABLED` | Block writes containing hardcoded machine-specific paths | +| `block_no_verify_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_BLOCK_NO_VERIFY_ENABLED` | Block git hook-bypass attempts (--no-verify, core.hooksPath=, hook-manager env-var disables for a configurable set — lefthook/husky/pre-commit/simple-git-hooks by default) | +| `block_dangerous_git_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_BLOCK_DANGEROUS_GIT_ENABLED` | Block irreversible git operations (push --force, push --force-with-lease leasing against a value git resolves at push time — either no expected value, or an expectation that is not an object id of the repository's own hash width — reset --hard, clean -f, worktree-wide checkout/restore discards) | +| `block_hook_bypass_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_BLOCK_HOOK_BYPASS_ENABLED` | Block Bash file-write workarounds that circumvent Write/Edit hook gates | +| `block_noncanonical_commit_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_BLOCK_NONCANONICAL_COMMIT_ENABLED` | Block `git commit -m` when the message actually contains a newline (multi-line `-m` mangles across shells — pipe it via `-F -` instead; single-line `-m` passes); --amend, -C/-c, --fixup/--squash, -F , and an in-progress merge/rebase are exempt | +| `block_convention_gate_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_BLOCK_CONVENTION_GATE_ENABLED` | Block a commit subject or `gh pr create --title` that violates the team-tracked convention pattern in .claude/source-control.md (no tracked pattern = no enforcement; same exemptions as block-noncanonical-commit) | +| `cli_flag_verify_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_CLI_FLAG_VERIFY_ENABLED` | Advise on hallucinated CLI flags written to files (never blocks) | +| `skill_reference_verify_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_SKILL_REFERENCE_VERIFY_ENABLED` | Advise when markdown cites a /plugin:skill reference this repo owns but cannot resolve (never blocks) | +| `stale_path_verify_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_STALE_PATH_VERIFY_ENABLED` | Advise when markdown cites a repo-relative path this repo's own history shows was removed and that is gone from the working tree (never blocks) | +| `workflow_resilience_check_enabled` | boolean | `false` | `CLAUDE_PLUGIN_OPTION_WORKFLOW_RESILIENCE_CHECK_ENABLED` | Advise on un-throttled Workflow fan-out (never blocks). Default off since 0.20.0: a behavioral-class prose injector, config-disabled per the instruction-economy evidence gate (#2021) — set true to opt back in | +| `flag_commit_pr_skill_bypass_enabled` | boolean | `false` | `CLAUDE_PLUGIN_OPTION_FLAG_COMMIT_PR_SKILL_BYPASS_ENABLED` | Advise when a direct gh pr create bypasses the source-control pull-request skill (never blocks). Default off since 0.20.0: a behavioral-class prose injector, config-disabled per the instruction-economy evidence gate (#2021) — set true to opt back in | +| `cli_flag_verify_bins` | string | *(none)* | `CLAUDE_PLUGIN_OPTION_CLI_FLAG_VERIFY_BINS` | Comma-separated binaries cli-flag-verify scans; empty uses the built-in default set | +| `cli_flag_verify_skip_bins` | string | *(none)* | `CLAUDE_PLUGIN_OPTION_CLI_FLAG_VERIFY_SKIP_BINS` | Comma-separated binaries cli-flag-verify must never scan | +| `block_dangerous_git_allow` | string | *(none)* | `CLAUDE_PLUGIN_OPTION_BLOCK_DANGEROUS_GIT_ALLOW` | Comma-separated forms block-dangerous-git permits: push-force, push-lease-unsafe, reset-hard, clean-force, checkout-dot, restore-dot, checkout-force; empty blocks all | +| `block_noncanonical_commit_allow` | string | *(none)* | `CLAUDE_PLUGIN_OPTION_BLOCK_NONCANONICAL_COMMIT_ALLOW` | Comma-separated form tokens to allow (currently: message-flag, which permits `-m` even when the message contains a newline) | +| `block_no_verify_hook_manager_prefixes` | string | *(none)* | `CLAUDE_PLUGIN_OPTION_BLOCK_NO_VERIFY_HOOK_MANAGER_PREFIXES` | Comma-separated hook-manager env-var name prefixes block-no-verify treats as a bypass when set to 0/false (e.g. lefthook,husky); empty uses the built-in default set (lefthook, husky, pre_commit, simple_git_hooks) | +| `stdin_read_timeout` | number
*min 1* | `2` | `CLAUDE_PLUGIN_OPTION_STDIN_READ_TIMEOUT` | Idle bound on reading the hook payload from stdin — how long a silent pipe is tolerated before a blocking guard fails closed | + +### How to set these + +Three supported routes, in the order most people want them: + +1. **Interactively** — Claude Code prompts for declared options when you enable the + plugin. To change them later: `/plugin configure guardrails`. +2. **Headless, at install time** — repeat `--config` for each option. Replace + `` with the marketplace you installed this plugin from: + + ```shell + claude plugin install guardrails@ --config secret_pattern_detection_enabled= + ``` + +3. **By hand, in settings** — add the value under `pluginConfigs` in your **user** + settings (`~/.claude/settings.json`): + + ```json + { + "pluginConfigs": { + "guardrails@": { + "options": { + "secret_pattern_detection_enabled": + } + } + } + } + ``` + + Plugin option values are read from **user**, `--settings`, and managed settings + only — **not** from a project's `.claude/settings.json`. To vary behavior per + repository, enable or disable the plugin in that project's `enabledPlugins` + instead of setting an option there. + +Do not set the `CLAUDE_PLUGIN_OPTION_*` variables yourself. They are how Claude Code +hands a configured value to a hook process; the value comes from the routes above. + +### Upstream documentation + +- [User configuration](https://code.claude.com/docs/en/plugins-reference#user-configuration) — the `userConfig` schema and the `CLAUDE_PLUGIN_OPTION_` export +- [Plugin settings](https://code.claude.com/docs/en/settings#plugin-settings) — `enabledPlugins`, `extraKnownMarketplaces`, `pluginConfigs` +- [Configuration scopes](https://code.claude.com/docs/en/settings#configuration-scopes) — user vs project vs local precedence +- [Manage installed plugins](https://code.claude.com/docs/en/discover-plugins#manage-installed-plugins) — enabling, disabling, `/plugin list` + + + ## License MIT (SPDX-License-Identifier: MIT). diff --git a/plugins/guardrails/hooks/hook-utils.sh b/plugins/guardrails/hooks/hook-utils.sh index dcb0e820f..e88315e72 100644 --- a/plugins/guardrails/hooks/hook-utils.sh +++ b/plugins/guardrails/hooks/hook-utils.sh @@ -16,11 +16,33 @@ readonly _HOOK_UTILS_LOADED=1 # read from the hook-process CLAUDE_PLUGIN_OPTION__ENABLED mirror. # Exits 0 (allow) if disabled. Place after source, before stdin parsing. # hook::check_enabled "MARKDOWN_FORMAT" # checks CLAUDE_PLUGIN_OPTION_MARKDOWN_FORMAT_ENABLED -hook::check_enabled() { +# +# Deliberately NOT layered with a marketplace-specific fleet switch. Claude Code +# already ships the coarse controls, and a parallel scheme here would become a +# second source of truth for the same question: +# * `--safe-mode` / `CLAUDE_CODE_SAFE_MODE` — start with every customization +# (CLAUDE.md, plugins, skills, hooks, MCP servers) disabled +# * `disableAllHooks` — disable all hooks and any custom status line +# * `claude plugin disable|enable ` — per-plugin, dependency-aware +# This helper stays scoped to the one thing it owns: the plugin's own +# `_enabled` userConfig boolean, surfaced to hook processes as the native +# `$CLAUDE_PLUGIN_OPTION_` mirror. + +# hook::is_enabled — the same check as a PREDICATE. Returns 0 when the +# plugin should run, 1 when it should not. For callers that must not terminate +# the process on a "disabled" answer. +# +# The statusline tee is exactly that caller: it is a TRANSPARENT WRAPPER around +# the user's real statusline, so exiting 0 on "disabled" would suppress the +# wrapped command's output and blank the status line. It needs to skip its own +# side effect and still pass through. +hook::is_enabled() { local var_name="CLAUDE_PLUGIN_OPTION_${1}_ENABLED" - if [[ "${!var_name:-true}" != "true" ]]; then - exit 0 - fi + [[ "${!var_name:-true}" == "true" ]] +} + +hook::check_enabled() { + hook::is_enabled "$1" || exit 0 } # --- Prerequisite visibility -------------------------------------------------- diff --git a/plugins/knowledge/README.md b/plugins/knowledge/README.md index d2a049563..7666649aa 100644 --- a/plugins/knowledge/README.md +++ b/plugins/knowledge/README.md @@ -110,6 +110,67 @@ options above tune yt-dlp authentication and throttling; **course-platform credentials are intentionally not** `userConfig` — they stay in shell env vars because a `sensitive` option persists as plaintext on Windows today. + + +### Options reference + +Generated from this plugin's `.claude-plugin/plugin.json`. Every option Claude Code +will prompt for when the plugin is enabled, with the environment variable each hook +reads it from. + +| Option | Type | Default | Environment variable | Description | +| --- | --- | --- | --- | --- | +| `library_dir` | directory | `"."` | `CLAUDE_PLUGIN_OPTION_LIBRARY_DIR` | Directory where synthesized knowledge artifacts land. Default is the consuming repo root; a relative value is resolved against the project directory. Portable non-project roots: an absolute path, a leading ~ (home-relative), or an environment-variable reference ${NAME} / %NAME% (e.g. ${KNOWLEDGE_CORPUS_DIR}) so a machine-varying root never needs a literal machine path in this stored value. A working-notes or artifacts convention declared in your own project's CLAUDE.md or rules takes precedence. | +| `yt_dlp_js_runtimes` | string | `"node"` | `CLAUDE_PLUGIN_OPTION_YT_DLP_JS_RUNTIMES` | JavaScript runtime yt-dlp uses for YouTube signature deciphering. Default 'node'. Set to 'off' to omit the --js-runtimes flag entirely. | +| `yt_dlp_cookies_file` | string | *(none)* | `CLAUDE_PLUGIN_OPTION_YT_DLP_COOKIES_FILE` | Path to a Netscape-format cookies.txt for authenticated YouTube acquisition. Empty by default (unauthenticated, with automatic browser-cookie fallback on a bot check). Never commit cookie files. | +| `yt_dlp_cookies_from_browser` | string | *(none)* | `CLAUDE_PLUGIN_OPTION_YT_DLP_COOKIES_FROM_BROWSER` | Browser to pull YouTube cookies from (e.g. chrome, firefox, edge), forcing one instead of the automatic platform-ordered fallback. Empty by default. A cookies file, when set, wins over this. | +| `max_concurrent_acquires` | number
*min 1, max 3* | `1` | `CLAUDE_PLUGIN_OPTION_MAX_CONCURRENT_ACQUIRES` | Cap on concurrent yt-dlp acquisition runs during a batch. Default 1; raising it increases HTTP 429 throttling risk. | + +### How to set these + +Three supported routes, in the order most people want them: + +1. **Interactively** — Claude Code prompts for declared options when you enable the + plugin. To change them later: `/plugin configure knowledge`. +2. **Headless, at install time** — repeat `--config` for each option. Replace + `` with the marketplace you installed this plugin from: + + ```shell + claude plugin install knowledge@ --config library_dir= + ``` + +3. **By hand, in settings** — add the value under `pluginConfigs` in your **user** + settings (`~/.claude/settings.json`): + + ```json + { + "pluginConfigs": { + "knowledge@": { + "options": { + "library_dir": + } + } + } + } + ``` + + Plugin option values are read from **user**, `--settings`, and managed settings + only — **not** from a project's `.claude/settings.json`. To vary behavior per + repository, enable or disable the plugin in that project's `enabledPlugins` + instead of setting an option there. + +Do not set the `CLAUDE_PLUGIN_OPTION_*` variables yourself. They are how Claude Code +hands a configured value to a hook process; the value comes from the routes above. + +### Upstream documentation + +- [User configuration](https://code.claude.com/docs/en/plugins-reference#user-configuration) — the `userConfig` schema and the `CLAUDE_PLUGIN_OPTION_` export +- [Plugin settings](https://code.claude.com/docs/en/settings#plugin-settings) — `enabledPlugins`, `extraKnownMarketplaces`, `pluginConfigs` +- [Configuration scopes](https://code.claude.com/docs/en/settings#configuration-scopes) — user vs project vs local precedence +- [Manage installed plugins](https://code.claude.com/docs/en/discover-plugins#manage-installed-plugins) — enabling, disabling, `/plugin list` + + + ## License MIT (SPDX-License-Identifier: MIT). diff --git a/plugins/machine-health/README.md b/plugins/machine-health/README.md index 1c880e9d0..35666d7a3 100644 --- a/plugins/machine-health/README.md +++ b/plugins/machine-health/README.md @@ -82,6 +82,63 @@ mocks Win32/MSFT CIM types that resolve only there: pwsh -NoProfile -File plugins/machine-health/skills/audit/tests/Invoke-MachineHealthTests.ps1 ``` + + +### Options reference + +Generated from this plugin's `.claude-plugin/plugin.json`. Every option Claude Code +will prompt for when the plugin is enabled, with the environment variable each hook +reads it from. + +| Option | Type | Default | Environment variable | Description | +| --- | --- | --- | --- | --- | +| `report_dir` | directory | *(none)* | `CLAUDE_PLUGIN_OPTION_REPORT_DIR` | Directory where per-run health reports (reports/health-.md, e.g. health-2026-07-12T153327123Z.md) are written. Leave unset to use the default: Documents\MachineHealth under your user profile. Machine state (history, approvals, logs) is separate and always lives in the plugin data directory. | + +### How to set these + +Three supported routes, in the order most people want them: + +1. **Interactively** — Claude Code prompts for declared options when you enable the + plugin. To change them later: `/plugin configure machine-health`. +2. **Headless, at install time** — repeat `--config` for each option. Replace + `` with the marketplace you installed this plugin from: + + ```shell + claude plugin install machine-health@ --config report_dir= + ``` + +3. **By hand, in settings** — add the value under `pluginConfigs` in your **user** + settings (`~/.claude/settings.json`): + + ```json + { + "pluginConfigs": { + "machine-health@": { + "options": { + "report_dir": + } + } + } + } + ``` + + Plugin option values are read from **user**, `--settings`, and managed settings + only — **not** from a project's `.claude/settings.json`. To vary behavior per + repository, enable or disable the plugin in that project's `enabledPlugins` + instead of setting an option there. + +Do not set the `CLAUDE_PLUGIN_OPTION_*` variables yourself. They are how Claude Code +hands a configured value to a hook process; the value comes from the routes above. + +### Upstream documentation + +- [User configuration](https://code.claude.com/docs/en/plugins-reference#user-configuration) — the `userConfig` schema and the `CLAUDE_PLUGIN_OPTION_` export +- [Plugin settings](https://code.claude.com/docs/en/settings#plugin-settings) — `enabledPlugins`, `extraKnownMarketplaces`, `pluginConfigs` +- [Configuration scopes](https://code.claude.com/docs/en/settings#configuration-scopes) — user vs project vs local precedence +- [Manage installed plugins](https://code.claude.com/docs/en/discover-plugins#manage-installed-plugins) — enabling, disabling, `/plugin list` + + + ## License MIT (SPDX-License-Identifier: MIT). diff --git a/plugins/markdown-format/.claude-plugin/plugin.json b/plugins/markdown-format/.claude-plugin/plugin.json index 4ba070a90..fe360431e 100644 --- a/plugins/markdown-format/.claude-plugin/plugin.json +++ b/plugins/markdown-format/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "markdown-format", - "version": "0.11.5", + "version": "0.11.6", "description": "Auto-format and lint Markdown on edit via markdownlint-cli2 — only in repos that carry their own markdownlint config.", "author": { "name": "Melodic Software", diff --git a/plugins/markdown-format/CHANGELOG.md b/plugins/markdown-format/CHANGELOG.md index c50a63de8..c5776e9b7 100644 --- a/plugins/markdown-format/CHANGELOG.md +++ b/plugins/markdown-format/CHANGELOG.md @@ -3,6 +3,16 @@ All notable changes to the `markdown-format` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.11.6] + +### Changed + +- **Carries the shared hook library's new `hook::is_enabled` predicate.** `hook::check_enabled` + exits the process when a plugin is gated off, which is correct for a hook but wrong for a + caller that must keep running afterward. The resolution is now also available as a predicate + that returns instead of exiting. No behaviour of this plugin changes; the version moves so + consumers receive the updated library. + ## [0.11.5] ### Changed diff --git a/plugins/markdown-format/README.md b/plugins/markdown-format/README.md index d6d817a8e..56399aa55 100644 --- a/plugins/markdown-format/README.md +++ b/plugins/markdown-format/README.md @@ -170,6 +170,65 @@ These options are user-scoped (stored in your user settings, not the project's). To disable formatting for a single repository, disable the whole plugin in that project's `enabledPlugins` instead. + + +### Options reference + +Generated from this plugin's `.claude-plugin/plugin.json`. Every option Claude Code +will prompt for when the plugin is enabled, with the environment variable each hook +reads it from. + +| Option | Type | Default | Environment variable | Description | +| --- | --- | --- | --- | --- | +| `markdown_format_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_MARKDOWN_FORMAT_ENABLED` | Auto-format and lint Markdown on Write/Edit of .md/.mdc files (runs only when the repo carries a markdownlint config) | +| `markdown_format_lint_gitignored` | boolean | `false` | `CLAUDE_PLUGIN_OPTION_MARKDOWN_FORMAT_LINT_GITIGNORED` | By default the hook leaves gitignored files alone — a scratch tier the repo excludes is neither rewritten nor reported on. Set true to bypass THIS HOOK's git check; markdownlint-cli2's own ignores/gitignore config still applies downstream, so a path your markdownlint config also excludes stays untouched. | +| `markdown_format_max_findings` | number
*min 0* | `20` | `CLAUDE_PLUGIN_OPTION_MARKDOWN_FORMAT_MAX_FINDINGS` | How many individual markdownlint violations are listed per run. The total count and the leading rule codes are always reported regardless. 0 = unlimited. | + +### How to set these + +Three supported routes, in the order most people want them: + +1. **Interactively** — Claude Code prompts for declared options when you enable the + plugin. To change them later: `/plugin configure markdown-format`. +2. **Headless, at install time** — repeat `--config` for each option. Replace + `` with the marketplace you installed this plugin from: + + ```shell + claude plugin install markdown-format@ --config markdown_format_enabled= + ``` + +3. **By hand, in settings** — add the value under `pluginConfigs` in your **user** + settings (`~/.claude/settings.json`): + + ```json + { + "pluginConfigs": { + "markdown-format@": { + "options": { + "markdown_format_enabled": + } + } + } + } + ``` + + Plugin option values are read from **user**, `--settings`, and managed settings + only — **not** from a project's `.claude/settings.json`. To vary behavior per + repository, enable or disable the plugin in that project's `enabledPlugins` + instead of setting an option there. + +Do not set the `CLAUDE_PLUGIN_OPTION_*` variables yourself. They are how Claude Code +hands a configured value to a hook process; the value comes from the routes above. + +### Upstream documentation + +- [User configuration](https://code.claude.com/docs/en/plugins-reference#user-configuration) — the `userConfig` schema and the `CLAUDE_PLUGIN_OPTION_` export +- [Plugin settings](https://code.claude.com/docs/en/settings#plugin-settings) — `enabledPlugins`, `extraKnownMarketplaces`, `pluginConfigs` +- [Configuration scopes](https://code.claude.com/docs/en/settings#configuration-scopes) — user vs project vs local precedence +- [Manage installed plugins](https://code.claude.com/docs/en/discover-plugins#manage-installed-plugins) — enabling, disabling, `/plugin list` + + + ## License MIT (SPDX-License-Identifier: MIT). diff --git a/plugins/markdown-format/hooks/hook-utils.sh b/plugins/markdown-format/hooks/hook-utils.sh index dcb0e820f..e88315e72 100644 --- a/plugins/markdown-format/hooks/hook-utils.sh +++ b/plugins/markdown-format/hooks/hook-utils.sh @@ -16,11 +16,33 @@ readonly _HOOK_UTILS_LOADED=1 # read from the hook-process CLAUDE_PLUGIN_OPTION__ENABLED mirror. # Exits 0 (allow) if disabled. Place after source, before stdin parsing. # hook::check_enabled "MARKDOWN_FORMAT" # checks CLAUDE_PLUGIN_OPTION_MARKDOWN_FORMAT_ENABLED -hook::check_enabled() { +# +# Deliberately NOT layered with a marketplace-specific fleet switch. Claude Code +# already ships the coarse controls, and a parallel scheme here would become a +# second source of truth for the same question: +# * `--safe-mode` / `CLAUDE_CODE_SAFE_MODE` — start with every customization +# (CLAUDE.md, plugins, skills, hooks, MCP servers) disabled +# * `disableAllHooks` — disable all hooks and any custom status line +# * `claude plugin disable|enable ` — per-plugin, dependency-aware +# This helper stays scoped to the one thing it owns: the plugin's own +# `_enabled` userConfig boolean, surfaced to hook processes as the native +# `$CLAUDE_PLUGIN_OPTION_` mirror. + +# hook::is_enabled — the same check as a PREDICATE. Returns 0 when the +# plugin should run, 1 when it should not. For callers that must not terminate +# the process on a "disabled" answer. +# +# The statusline tee is exactly that caller: it is a TRANSPARENT WRAPPER around +# the user's real statusline, so exiting 0 on "disabled" would suppress the +# wrapped command's output and blank the status line. It needs to skip its own +# side effect and still pass through. +hook::is_enabled() { local var_name="CLAUDE_PLUGIN_OPTION_${1}_ENABLED" - if [[ "${!var_name:-true}" != "true" ]]; then - exit 0 - fi + [[ "${!var_name:-true}" == "true" ]] +} + +hook::check_enabled() { + hook::is_enabled "$1" || exit 0 } # --- Prerequisite visibility -------------------------------------------------- diff --git a/plugins/miro/README.md b/plugins/miro/README.md index 7afd3af3c..2ebbbfd75 100644 --- a/plugins/miro/README.md +++ b/plugins/miro/README.md @@ -82,3 +82,60 @@ npm run verify-bundle # fail if dist/index.min.js drifts from src/ After editing `src/`, run `npm run bundle` and commit the regenerated `dist/index.min.js` alongside the source change. + + + +### Options reference + +Generated from this plugin's `.claude-plugin/plugin.json`. Every option Claude Code +will prompt for when the plugin is enabled, with the environment variable each hook +reads it from. + +| Option | Type | Default | Environment variable | Description | +| --- | --- | --- | --- | --- | +| `miro_api_token` | string
*required* | *(none)* | `CLAUDE_PLUGIN_OPTION_MIRO_API_TOKEN` | **Sensitive** — stored in the OS keychain or protected credentials file. Miro REST API token from https://miro.com/app/settings/user-profile/apps. Required — the bundled MCP server exits at startup without it. Stored by Claude Code in secure credential storage, never settings.json. | + +### How to set these + +Three supported routes, in the order most people want them: + +1. **Interactively** — Claude Code prompts for declared options when you enable the + plugin. To change them later: `/plugin configure miro`. +2. **Headless, at install time** — repeat `--config` for each option. Replace + `` with the marketplace you installed this plugin from: + + ```shell + claude plugin install miro@ --config miro_api_token= + ``` + +3. **By hand, in settings** — add the value under `pluginConfigs` in your **user** + settings (`~/.claude/settings.json`): + + ```json + { + "pluginConfigs": { + "miro@": { + "options": { + "miro_api_token": + } + } + } + } + ``` + + Plugin option values are read from **user**, `--settings`, and managed settings + only — **not** from a project's `.claude/settings.json`. To vary behavior per + repository, enable or disable the plugin in that project's `enabledPlugins` + instead of setting an option there. + +Do not set the `CLAUDE_PLUGIN_OPTION_*` variables yourself. They are how Claude Code +hands a configured value to a hook process; the value comes from the routes above. + +### Upstream documentation + +- [User configuration](https://code.claude.com/docs/en/plugins-reference#user-configuration) — the `userConfig` schema and the `CLAUDE_PLUGIN_OPTION_` export +- [Plugin settings](https://code.claude.com/docs/en/settings#plugin-settings) — `enabledPlugins`, `extraKnownMarketplaces`, `pluginConfigs` +- [Configuration scopes](https://code.claude.com/docs/en/settings#configuration-scopes) — user vs project vs local precedence +- [Manage installed plugins](https://code.claude.com/docs/en/discover-plugins#manage-installed-plugins) — enabling, disabling, `/plugin list` + + diff --git a/plugins/planning/README.md b/plugins/planning/README.md index 5aae9e4c8..2024e765f 100644 --- a/plugins/planning/README.md +++ b/plugins/planning/README.md @@ -65,6 +65,64 @@ self-ignoring `//` (default `.work/`). Run concern file `.claude/topic-docs.yaml` (`contract_dir`, `memory_dir`, `contract_tier: branch | local`); absent keys mean those documented defaults. + + +### Options reference + +Generated from this plugin's `.claude-plugin/plugin.json`. Every option Claude Code +will prompt for when the plugin is enabled, with the environment variable each hook +reads it from. + +| Option | Type | Default | Environment variable | Description | +| --- | --- | --- | --- | --- | +| `use_ask_user_question` | boolean | `false` | `CLAUDE_PLUGIN_OPTION_USE_ASK_USER_QUESTION` | When enabled, the planning skills' question rounds (interview, prd, design, plan) render a round of up to 4 independent questions through the AskUserQuestion tool instead of inline prose. Default: inline prose (dictation-friendly). | +| `use_emoji_question_markers` | boolean | `false` | `CLAUDE_PLUGIN_OPTION_USE_EMOJI_QUESTION_MARKERS` | When enabled, each inline interview round question leads with a ❓ anchor on its Q line and its 'My recommendation:' line leads with ➡️. Purely presentational — Q numbering stays the functional handle, and persisted artifacts (ledger, register, Brief) never carry the emoji. Default: plain text. | + +### How to set these + +Three supported routes, in the order most people want them: + +1. **Interactively** — Claude Code prompts for declared options when you enable the + plugin. To change them later: `/plugin configure planning`. +2. **Headless, at install time** — repeat `--config` for each option. Replace + `` with the marketplace you installed this plugin from: + + ```shell + claude plugin install planning@ --config use_ask_user_question= + ``` + +3. **By hand, in settings** — add the value under `pluginConfigs` in your **user** + settings (`~/.claude/settings.json`): + + ```json + { + "pluginConfigs": { + "planning@": { + "options": { + "use_ask_user_question": + } + } + } + } + ``` + + Plugin option values are read from **user**, `--settings`, and managed settings + only — **not** from a project's `.claude/settings.json`. To vary behavior per + repository, enable or disable the plugin in that project's `enabledPlugins` + instead of setting an option there. + +Do not set the `CLAUDE_PLUGIN_OPTION_*` variables yourself. They are how Claude Code +hands a configured value to a hook process; the value comes from the routes above. + +### Upstream documentation + +- [User configuration](https://code.claude.com/docs/en/plugins-reference#user-configuration) — the `userConfig` schema and the `CLAUDE_PLUGIN_OPTION_` export +- [Plugin settings](https://code.claude.com/docs/en/settings#plugin-settings) — `enabledPlugins`, `extraKnownMarketplaces`, `pluginConfigs` +- [Configuration scopes](https://code.claude.com/docs/en/settings#configuration-scopes) — user vs project vs local precedence +- [Manage installed plugins](https://code.claude.com/docs/en/discover-plugins#manage-installed-plugins) — enabling, disabling, `/plugin list` + + + ## License MIT (SPDX-License-Identifier: MIT). diff --git a/plugins/powershell-format/.claude-plugin/plugin.json b/plugins/powershell-format/.claude-plugin/plugin.json index 81c687c2d..b1ddc9be7 100644 --- a/plugins/powershell-format/.claude-plugin/plugin.json +++ b/plugins/powershell-format/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "powershell-format", - "version": "0.7.3", + "version": "0.7.4", "description": "Auto-format and lint PowerShell on edit via PSScriptAnalyzer, only when a PSScriptAnalyzerSettings.psd1 governs the repo — using the consuming repo's own analyzer settings.", "author": { "name": "Melodic Software", diff --git a/plugins/powershell-format/CHANGELOG.md b/plugins/powershell-format/CHANGELOG.md index a1d6455ca..0d507e95e 100644 --- a/plugins/powershell-format/CHANGELOG.md +++ b/plugins/powershell-format/CHANGELOG.md @@ -3,6 +3,16 @@ All notable changes to the `powershell-format` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.7.4] + +### Changed + +- **Carries the shared hook library's new `hook::is_enabled` predicate.** `hook::check_enabled` + exits the process when a plugin is gated off, which is correct for a hook but wrong for a + caller that must keep running afterward. The resolution is now also available as a predicate + that returns instead of exiting. No behaviour of this plugin changes; the version moves so + consumers receive the updated library. + ## [0.7.3] ### Fixed diff --git a/plugins/powershell-format/README.md b/plugins/powershell-format/README.md index c61e777e9..a41fb493c 100644 --- a/plugins/powershell-format/README.md +++ b/plugins/powershell-format/README.md @@ -115,6 +115,63 @@ These options are user-scoped (stored in your user settings, not the project's). To turn the hook off for a single repository, disable the whole plugin in that project's `enabledPlugins` instead. + + +### Options reference + +Generated from this plugin's `.claude-plugin/plugin.json`. Every option Claude Code +will prompt for when the plugin is enabled, with the environment variable each hook +reads it from. + +| Option | Type | Default | Environment variable | Description | +| --- | --- | --- | --- | --- | +| `powershell_format_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_POWERSHELL_FORMAT_ENABLED` | Format and lint PowerShell on edit via PSScriptAnalyzer | + +### How to set these + +Three supported routes, in the order most people want them: + +1. **Interactively** — Claude Code prompts for declared options when you enable the + plugin. To change them later: `/plugin configure powershell-format`. +2. **Headless, at install time** — repeat `--config` for each option. Replace + `` with the marketplace you installed this plugin from: + + ```shell + claude plugin install powershell-format@ --config powershell_format_enabled= + ``` + +3. **By hand, in settings** — add the value under `pluginConfigs` in your **user** + settings (`~/.claude/settings.json`): + + ```json + { + "pluginConfigs": { + "powershell-format@": { + "options": { + "powershell_format_enabled": + } + } + } + } + ``` + + Plugin option values are read from **user**, `--settings`, and managed settings + only — **not** from a project's `.claude/settings.json`. To vary behavior per + repository, enable or disable the plugin in that project's `enabledPlugins` + instead of setting an option there. + +Do not set the `CLAUDE_PLUGIN_OPTION_*` variables yourself. They are how Claude Code +hands a configured value to a hook process; the value comes from the routes above. + +### Upstream documentation + +- [User configuration](https://code.claude.com/docs/en/plugins-reference#user-configuration) — the `userConfig` schema and the `CLAUDE_PLUGIN_OPTION_` export +- [Plugin settings](https://code.claude.com/docs/en/settings#plugin-settings) — `enabledPlugins`, `extraKnownMarketplaces`, `pluginConfigs` +- [Configuration scopes](https://code.claude.com/docs/en/settings#configuration-scopes) — user vs project vs local precedence +- [Manage installed plugins](https://code.claude.com/docs/en/discover-plugins#manage-installed-plugins) — enabling, disabling, `/plugin list` + + + ## License MIT (SPDX-License-Identifier: MIT). diff --git a/plugins/powershell-format/hooks/hook-utils.sh b/plugins/powershell-format/hooks/hook-utils.sh index dcb0e820f..e88315e72 100644 --- a/plugins/powershell-format/hooks/hook-utils.sh +++ b/plugins/powershell-format/hooks/hook-utils.sh @@ -16,11 +16,33 @@ readonly _HOOK_UTILS_LOADED=1 # read from the hook-process CLAUDE_PLUGIN_OPTION__ENABLED mirror. # Exits 0 (allow) if disabled. Place after source, before stdin parsing. # hook::check_enabled "MARKDOWN_FORMAT" # checks CLAUDE_PLUGIN_OPTION_MARKDOWN_FORMAT_ENABLED -hook::check_enabled() { +# +# Deliberately NOT layered with a marketplace-specific fleet switch. Claude Code +# already ships the coarse controls, and a parallel scheme here would become a +# second source of truth for the same question: +# * `--safe-mode` / `CLAUDE_CODE_SAFE_MODE` — start with every customization +# (CLAUDE.md, plugins, skills, hooks, MCP servers) disabled +# * `disableAllHooks` — disable all hooks and any custom status line +# * `claude plugin disable|enable ` — per-plugin, dependency-aware +# This helper stays scoped to the one thing it owns: the plugin's own +# `_enabled` userConfig boolean, surfaced to hook processes as the native +# `$CLAUDE_PLUGIN_OPTION_` mirror. + +# hook::is_enabled — the same check as a PREDICATE. Returns 0 when the +# plugin should run, 1 when it should not. For callers that must not terminate +# the process on a "disabled" answer. +# +# The statusline tee is exactly that caller: it is a TRANSPARENT WRAPPER around +# the user's real statusline, so exiting 0 on "disabled" would suppress the +# wrapped command's output and blank the status line. It needs to skip its own +# side effect and still pass through. +hook::is_enabled() { local var_name="CLAUDE_PLUGIN_OPTION_${1}_ENABLED" - if [[ "${!var_name:-true}" != "true" ]]; then - exit 0 - fi + [[ "${!var_name:-true}" == "true" ]] +} + +hook::check_enabled() { + hook::is_enabled "$1" || exit 0 } # --- Prerequisite visibility -------------------------------------------------- diff --git a/plugins/rate-limit-guard/.claude-plugin/plugin.json b/plugins/rate-limit-guard/.claude-plugin/plugin.json index fdc2835fc..1420f48c7 100644 --- a/plugins/rate-limit-guard/.claude-plugin/plugin.json +++ b/plugins/rate-limit-guard/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "rate-limit-guard", - "version": "0.5.4", + "version": "0.5.7", "description": "Shared rate-limit guard for loop lanes: a statusline wrapper tees the subscription rate-limit windows to a fixed machine-scope file, a StopFailure hook records rate-limit stops reactively, and a reader contract fixes how consuming sessions pause and resume.", "author": { "name": "Melodic Software", @@ -20,7 +20,7 @@ "rate_limit_guard_enabled": { "type": "boolean", "title": "rate-limit-guard hook kill switch", - "description": "Master switch for the StopFailure detection hook", + "description": "Master switch for the StopFailure detection hook and the statusline tee's snapshot write; read from managed settings first, then user settings", "default": true } } diff --git a/plugins/rate-limit-guard/CHANGELOG.md b/plugins/rate-limit-guard/CHANGELOG.md index 660bf6bc1..03c5796e0 100644 --- a/plugins/rate-limit-guard/CHANGELOG.md +++ b/plugins/rate-limit-guard/CHANGELOG.md @@ -3,6 +3,111 @@ All notable changes to the `rate-limit-guard` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.5.7] + +### Fixed + +- **The statusline tee's kill switch read only one of the three scopes the contract names, so a + managed policy was silently ignored.** `0.5.6` moved the gate onto a direct settings read but + implemented the user settings file alone. This repository's own + [hook-config-delivery](../../docs/conventions/hook-config-delivery/README.md) convention, fact 5, + states that `pluginConfigs` is read back from **user settings, the `--settings` flag, and managed + settings** — so an organization that set `rate_limit_guard_enabled: false` in + `managed-settings.json` had the tee keep writing anyway. Managed settings are the + highest-precedence scope and cannot be overridden by any user or project scope, which is exactly + what makes that a policy bypass rather than a cosmetic omission. + + The gate now reads managed settings too, mirroring the channel-F exemplars the convention points + at — `plugins/disk-hygiene/lib/killswitch_config.py` and the sibling bash reader + `plugins/autonomy/hooks/lane-stop-gate-lib.sh`: the fixed per-platform root-owned paths + (`/Library/Application Support/ClaudeCode/`, `/etc/claude-code/`, `C:/Program Files/ClaudeCode/`) + selected by `uname -s`, plus the `managed-settings.d/` drop-ins in sorted order with later files + overriding earlier ones. The Windows path is the literal absolute path the docs give, never + `%ProgramFiles%`-derived, and every resolved path is re-checked as absolute — an + environment-derived or relative base would let a repository redirect the one scope that outranks + every other. + + **Precedence is now managed → user settings → environment**, highest first. Managed wins because + a gate a user or a repository can out-vote is not a policy control. The environment channel also + moved *below* user settings, which is a second behaviour change and deliberate: it is retained + only in case `CLAUDE_PLUGIN_OPTION_RATE_LIMIT_GUARD_ENABLED` is ever delivered to a `statusLine` + process, and for an unconfigured key a repository `.claude/settings.json` `env` block populates it + freely with no provenance (same convention, fact 4), so it must not out-vote a value a real + settings scope configured. Every previously held property survives: the tee fails **open** on a + missing file, missing `jq`, malformed JSON, or an unrecognized platform; the `pluginConfigs` key + is still matched by prefix so a fork or private catalog works; and the jq filter still avoids + `// empty` on the value — the alternative operator treats `false` as falsy and would discard the + exact value this gate exists to detect — using `tostring` plus an explicit `length == 0` emptiness + test instead. + + **Residuals (accepted, unchanged by this release).** The *user* settings file is still located + from `${CLAUDE_CONFIG_DIR:-$HOME/.claude}` rather than channel F's install-cache anchor, so a + repository `env` block that redirects `CLAUDE_CONFIG_DIR` can still hide a user-scope `false`; + and a value supplied only through a session `--settings` file is invisible to any on-disk read + (channel F's own documented residual). Neither reaches the managed verdict, which is + environment-independent by construction, so the scope an organization actually enforces with is + now sound. + +- **Nothing tested the gate at all.** `scripts/statusline-tee.test.sh` had no coverage of + `_rlg_tee_enabled` under either implementation; the `0.5.5` version passed review only because + the tests injected the environment variable by hand, and `0.5.6` carried the same gap forward. + The suite now drives the gate through real settings files on a scoped `HOME`: unconfigured (no + file, and a file with no `pluginConfigs`), user `false` and user `true`, a `false` under a + different marketplace suffix (the prefix match), another plugin's identically-named option and a + prefix-colliding plugin name, malformed JSON and a missing `jq` (both fail open), and managed + `false` over user `true` *and* managed `true` over user `false` — the mirror case is what + distinguishes real precedence from an or-of-falses. Every case also asserts that the wrapped + statusline's stdout is unchanged, because a gate that blanked the status line would be worse than + the bug it closes; one unstubbed end-to-end case exercises the script exactly as `settings.json` + invokes it. + + Managed settings live at fixed root-owned paths a test cannot write, so those cases source the + wrapper and stub the path list. To make that possible the script gained a `main` function behind + the `[[ "${BASH_SOURCE[0]}" == "${0}" ]]` sourcing guard already used elsewhere in this + repository, and its stdin read moved inside it; a direct invocation behaves exactly as before. + +- **The plugin's own documentation still described the pre-`0.5.5` single-surface switch.** Both + the manifest's option `description` (which is what `/plugin configure` shows) and the README's + `## Configuration` section called `rate_limit_guard_enabled` the kill switch for the StopFailure + hook alone, and the README additionally told operators that "disabling the statusline tee is the + operator's edit" — true before `0.5.5` gated the tee's write on the same option, wrong since. + Both now say the switch governs the hook **and** the tee's snapshot write, and the README states + where each surface reads it from and that the tee's precedence is managed → user → environment, + so an operator can tell why a managed value outranks the one they set themselves. + +## [0.5.6] + +### Fixed + +- **The statusline tee's kill switch read a channel that never reaches it.** The previous + release gated the tee on `CLAUDE_PLUGIN_OPTION_RATE_LIMIT_GUARD_ENABLED`, but Claude Code + exports `CLAUDE_PLUGIN_OPTION_` to **hook processes** only, and this script is invoked by + absolute path from the user's `statusLine` setting. The variable was therefore always unset, + the `:-true` fallback always won, and the gate was decorative -- it only appeared to work + because the tests injected the variable by hand. The tee now reads + `pluginConfigs.@.options.rate_limit_guard_enabled` from the user's + settings directly, the sanctioned route for a non-hook consumer. The `pluginConfigs` key is + matched by prefix so a fork or private catalog works, and every failure path -- no settings + file, no jq, malformed JSON -- still runs the tee. + +## [0.5.5] + +### Fixed + +- **The statusline tee ignored `rate_limit_guard_enabled` and wrote on every render regardless.** + `scripts/statusline-tee.sh` is invoked by absolute path from the user's `settings.json` + `statusLine`, not by the plugin hook runner, so it was reached whatever the plugin's enablement + said — it was the one code path in this plugin that kept running while the plugin was disabled, + rewriting `~/.claude/rate-limit-guard/rate-limits.json` on the statusline's refresh cadence. It + now consults the option before taking the snapshot. + + The gate uses the new `hook::is_enabled` predicate rather than `hook::check_enabled`. The tee is + a **transparent wrapper** around the user's real statusline: `check_enabled` exits 0, which here + would have suppressed the wrapped command's stdout and blanked the status line. Only the tee's + own write is skipped; the passthrough is unconditional and byte-identical either way. If the + shared library cannot be read the tee still runs, consistent with this script's existing rule + that no tee outcome ever alters the wrapped statusline. + ## [0.5.4] ### Fixed diff --git a/plugins/rate-limit-guard/README.md b/plugins/rate-limit-guard/README.md index 3a56afd1c..94d39344c 100644 --- a/plugins/rate-limit-guard/README.md +++ b/plugins/rate-limit-guard/README.md @@ -87,17 +87,26 @@ One `userConfig` option: | Option | What it controls | |---|---| -| `rate_limit_guard_enabled` | Kill switch for the StopFailure detection hook (default `true`). | +| `rate_limit_guard_enabled` | Kill switch for the StopFailure detection hook **and** the statusline tee's snapshot write (default `true`). | Set it with `/plugin configure rate-limit-guard`, or headless on a fresh install via `claude plugin install rate-limit-guard@ --config rate_limit_guard_enabled=false`. +**Where the switch is read from.** The StopFailure hook receives it the ordinary way, as +`CLAUDE_PLUGIN_OPTION_RATE_LIMIT_GUARD_ENABLED`. The statusline tee cannot: it is invoked by +absolute path from the operator's own `statusLine` setting, and Claude Code exports those variables +to *hook* processes only, so the tee reads `pluginConfigs` from the settings files directly. Its +precedence is **managed settings → user settings → environment**, highest first — a managed +`rate_limit_guard_enabled: false` is an organization's policy and wins over any user value. Every +degraded read (no settings file, no `jq`, malformed JSON) leaves the tee enabled, and no outcome of +that gate ever changes what your statusline prints. + The tee path and the 90% pause threshold are deliberately **not** configurable: they are contract constants that cross-plugin consumers inline from the [reader contract](reference/reader-contract.md); a per-user override would silently split the -writer from its readers. Disabling the statusline tee is the operator's edit (remove or unwrap the -statusline command); disabling the hook is the kill switch; disabling everything is -`enabledPlugins` / uninstall. +writer from its readers. The kill switch stops the tee's *write* while leaving the wrapper +transparent; removing the wrapper itself is the operator's edit to their `statusLine`; disabling +everything is `enabledPlugins` / uninstall. ## Consumers @@ -105,6 +114,63 @@ Written for the loop-lane convention's three lanes (work-items `work-loop` and ` source-control `babysit-loop`), which inline the reader contract's operable floor. Any session or tool on the machine may read the same files under the same contract. + + +### Options reference + +Generated from this plugin's `.claude-plugin/plugin.json`. Every option Claude Code +will prompt for when the plugin is enabled, with the environment variable each hook +reads it from. + +| Option | Type | Default | Environment variable | Description | +| --- | --- | --- | --- | --- | +| `rate_limit_guard_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_RATE_LIMIT_GUARD_ENABLED` | Master switch for the StopFailure detection hook and the statusline tee's snapshot write; read from managed settings first, then user settings | + +### How to set these + +Three supported routes, in the order most people want them: + +1. **Interactively** — Claude Code prompts for declared options when you enable the + plugin. To change them later: `/plugin configure rate-limit-guard`. +2. **Headless, at install time** — repeat `--config` for each option. Replace + `` with the marketplace you installed this plugin from: + + ```shell + claude plugin install rate-limit-guard@ --config rate_limit_guard_enabled= + ``` + +3. **By hand, in settings** — add the value under `pluginConfigs` in your **user** + settings (`~/.claude/settings.json`): + + ```json + { + "pluginConfigs": { + "rate-limit-guard@": { + "options": { + "rate_limit_guard_enabled": + } + } + } + } + ``` + + Plugin option values are read from **user**, `--settings`, and managed settings + only — **not** from a project's `.claude/settings.json`. To vary behavior per + repository, enable or disable the plugin in that project's `enabledPlugins` + instead of setting an option there. + +Do not set the `CLAUDE_PLUGIN_OPTION_*` variables yourself. They are how Claude Code +hands a configured value to a hook process; the value comes from the routes above. + +### Upstream documentation + +- [User configuration](https://code.claude.com/docs/en/plugins-reference#user-configuration) — the `userConfig` schema and the `CLAUDE_PLUGIN_OPTION_` export +- [Plugin settings](https://code.claude.com/docs/en/settings#plugin-settings) — `enabledPlugins`, `extraKnownMarketplaces`, `pluginConfigs` +- [Configuration scopes](https://code.claude.com/docs/en/settings#configuration-scopes) — user vs project vs local precedence +- [Manage installed plugins](https://code.claude.com/docs/en/discover-plugins#manage-installed-plugins) — enabling, disabling, `/plugin list` + + + ## License MIT (SPDX-License-Identifier: MIT). diff --git a/plugins/rate-limit-guard/hooks/hook-utils.sh b/plugins/rate-limit-guard/hooks/hook-utils.sh index dcb0e820f..e88315e72 100755 --- a/plugins/rate-limit-guard/hooks/hook-utils.sh +++ b/plugins/rate-limit-guard/hooks/hook-utils.sh @@ -16,11 +16,33 @@ readonly _HOOK_UTILS_LOADED=1 # read from the hook-process CLAUDE_PLUGIN_OPTION__ENABLED mirror. # Exits 0 (allow) if disabled. Place after source, before stdin parsing. # hook::check_enabled "MARKDOWN_FORMAT" # checks CLAUDE_PLUGIN_OPTION_MARKDOWN_FORMAT_ENABLED -hook::check_enabled() { +# +# Deliberately NOT layered with a marketplace-specific fleet switch. Claude Code +# already ships the coarse controls, and a parallel scheme here would become a +# second source of truth for the same question: +# * `--safe-mode` / `CLAUDE_CODE_SAFE_MODE` — start with every customization +# (CLAUDE.md, plugins, skills, hooks, MCP servers) disabled +# * `disableAllHooks` — disable all hooks and any custom status line +# * `claude plugin disable|enable ` — per-plugin, dependency-aware +# This helper stays scoped to the one thing it owns: the plugin's own +# `_enabled` userConfig boolean, surfaced to hook processes as the native +# `$CLAUDE_PLUGIN_OPTION_` mirror. + +# hook::is_enabled — the same check as a PREDICATE. Returns 0 when the +# plugin should run, 1 when it should not. For callers that must not terminate +# the process on a "disabled" answer. +# +# The statusline tee is exactly that caller: it is a TRANSPARENT WRAPPER around +# the user's real statusline, so exiting 0 on "disabled" would suppress the +# wrapped command's output and blank the status line. It needs to skip its own +# side effect and still pass through. +hook::is_enabled() { local var_name="CLAUDE_PLUGIN_OPTION_${1}_ENABLED" - if [[ "${!var_name:-true}" != "true" ]]; then - exit 0 - fi + [[ "${!var_name:-true}" == "true" ]] +} + +hook::check_enabled() { + hook::is_enabled "$1" || exit 0 } # --- Prerequisite visibility -------------------------------------------------- diff --git a/plugins/rate-limit-guard/scripts/statusline-tee.sh b/plugins/rate-limit-guard/scripts/statusline-tee.sh index 4da949155..686b8a398 100755 --- a/plugins/rate-limit-guard/scripts/statusline-tee.sh +++ b/plugins/rate-limit-guard/scripts/statusline-tee.sh @@ -69,11 +69,18 @@ INPUT="" # payload at the timeout (measured on Git Bash); Bash below 4.1 (macOS ships # 3.2) lacks -N and falls back to the delimiter form, fast enough on native # POSIX pipes. 1MiB bound: statusline payloads are a few KB. -if ((BASH_VERSINFO[0] > 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] >= 1))); then - IFS= read -r -N 1048576 -t 5 INPUT || true -else - IFS= read -r -d '' -t 5 INPUT || true -fi +# +# Deferred into a function rather than run at load: this file carries a sourcing +# guard at the bottom so its enablement gate can be driven by the test suite, and +# a top-level read would consume the sourcing shell's stdin before `main` runs. +read_tee_input() { + if ((BASH_VERSINFO[0] > 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] >= 1))); then + IFS= read -r -N 1048576 -t 5 INPUT || true + else + IFS= read -r -d '' -t 5 INPUT || true + fi + return 0 +} # Path of the temp file currently in flight, for the reclaim traps below. A # global rather than the function's local: the trap body is evaluated when the @@ -248,33 +255,197 @@ tee_snapshot() { return 0 } -tee_snapshot +# The tee's WRITE is plugin behavior and is gated like any other hook's. Two +# things make this call site different from a normal `hook::check_enabled`: +# +# 1. This script is a TRANSPARENT statusline wrapper. `hook::check_enabled` +# exits 0 on "disabled", which here would suppress the wrapped command's +# stdout and blank the user's status line. The predicate form skips only +# the snapshot; the passthrough below stays unconditional. +# 2. This script is invoked BY ABSOLUTE PATH from settings.json `statusLine`, +# not by the plugin hook runner, so it is reached whatever the plugin's +# enablement says. Without this gate it is the one code path in the plugin +# that keeps running when the plugin is off. +# +# If no scope contributes a verdict the tee still runs, consistent with this +# script's rule that no tee outcome ever alters the wrapped statusline. +# +# CHANNEL: this process is NOT a hook process. Claude Code exports +# CLAUDE_PLUGIN_OPTION_ to hook processes only (plugins-reference, "User +# configuration"; and this repo's docs/conventions/hook-config-delivery/README.md scopes +# the env channel the same way). Reading that variable alone would always find it unset, +# always fall back to enabled, and produce a gate that is decorative -- so the value is +# read from the configured sources directly (channel F), which is the sanctioned route +# for a non-hook consumer. The env var is still honored, but last (see the precedence +# note on _rlg_tee_enabled), in case it is ever delivered here. -if (($#)); then - # Wrapped mode: transparent passthrough. The wrapped command sees the same - # stdin bytes and owns stdout; its exit code is the wrapper's. pipefail is - # dropped for exactly this pipeline: a wrapped command that never reads - # stdin closes the pipe under printf, and pipefail would surface printf's - # SIGPIPE (141) instead of the wrapped command's own exit code. printf's - # stderr is silenced for the same case (bash prints a broken-pipe notice). - set +o pipefail - printf '%s' "$INPUT" 2>/dev/null | "$@" - rc=$? - set -o pipefail - if ! command -v jq >/dev/null 2>&1; then - printf 'rate-limit-guard: jq not found — rate-limit tee disabled (https://jqlang.org/download/)\n' +# Managed settings — the highest-precedence scope Claude Code honors for +# pluginConfigs (settings docs, "Settings precedence") and the one an organization +# uses to enforce a policy. Fixed, root-owned per-platform paths, the primary file +# first and then the `managed-settings.d/` drop-ins in sorted order; later files +# override earlier ones in _rlg_managed_option, mirroring Claude Code's merge. +# +# Platform comes from `uname -s`, never $OSTYPE — the same spelling as this repo's +# sibling bash channel-F reader, plugins/autonomy/hooks/lane-stop-gate-lib.sh, +# itself the bash equivalent of the python exemplar's sys.platform table +# (plugins/disk-hygiene/lib/killswitch_config.py::managed_settings_path). The +# Windows path is the literal absolute path the docs give, never +# %ProgramFiles%-derived: an environment-derived base would let a repo `env` block +# redirect the highest-precedence scope at the one process that trusts it most. +# +# Server-managed settings are deliberately excluded, as in the sibling reader: +# their only on-disk artifact is the user-writable cache +# ~/.claude/remote-settings.json, which fails this list's trust test. +# +# Prints nothing on an unrecognized platform — managed then configures nothing and +# the user scope decides. +_rlg_managed_settings_files() { + local primary + case "$(uname -s 2>/dev/null)" in + Darwin) primary="/Library/Application Support/ClaudeCode/managed-settings.json" ;; + MINGW* | MSYS* | CYGWIN*) primary="C:/Program Files/ClaudeCode/managed-settings.json" ;; + Linux) primary="/etc/claude-code/managed-settings.json" ;; + *) return 0 ;; + esac + # Defense in depth: a managed path MUST be absolute (POSIX /… or a Windows + # drive). A relative value would resolve against this process's cwd — the + # user's checkout — turning the highest-precedence scope into a plantable file. + case "$primary" in + /* | [A-Za-z]:[/\\]*) ;; + *) return 0 ;; + esac + [[ -f "$primary" ]] && printf '%s\n' "$primary" + local dropin="${primary%/*}/managed-settings.d" f + if [[ -d "$dropin" ]]; then + for f in "$dropin"/*.json; do + [[ -f "$f" ]] && printf '%s\n' "$f" + done fi - exit "$rc" -fi + return 0 +} + +# Print this plugin's configured rate_limit_guard_enabled from ONE settings file +# as "true"/"false"; return 1 when that file contributes no verdict — absent, +# unreadable, unparsable, no matching entry, or a JSON null. Every no-verdict path +# is a fail-OPEN path at the caller. +# +# NOT `// empty` on the value: jq's alternative operator treats `false` as falsy, +# so the one value this gate exists to detect would be discarded. `tostring` yields +# "true"/"false", and emptiness is decided by an explicit `length == 0` on the +# collected array, so no alternative operator ever touches a boolean. +# +# Marketplace-agnostic: pluginConfigs is keyed `@`, and this +# plugin may be installed from a fork or private catalog, so the key is matched by +# PREFIX. Last match wins, as in the sibling reader. +# +# The file is opened by bash (`<"$file"`), not by jq: a native jq on Windows +# cannot open an MSYS-style path, while a shell redirection always can. +_rlg_settings_option() { + local file="$1" out + [[ -r "$file" ]] || return 1 + command -v jq >/dev/null 2>&1 || return 1 + out="$(jq -r '[ (.pluginConfigs // {}) | to_entries[] + | select(.key | startswith("rate-limit-guard@")) + | .value.options.rate_limit_guard_enabled + | select(. != null) | tostring ] + | if length == 0 then empty else last end' <"$file" 2>/dev/null)" || return 1 + [[ -n "$out" ]] || return 1 + printf '%s' "$out" +} + +# The managed-scope verdict, or return 1 when managed configures none. +_rlg_managed_option() { + local f v verdict="" have=1 + while IFS= read -r f; do + [[ -n "$f" ]] || continue + if v="$(_rlg_settings_option "$f")"; then + verdict="$v" + have=0 + fi + done < <(_rlg_managed_settings_files) + ((have == 0)) || return 1 + printf '%s' "$verdict" +} + +# PRECEDENCE, highest first: managed settings, then user settings, then the +# environment. Absent a verdict from all three the tee runs — the plugin default, +# and the fail-OPEN direction every degraded read lands on. +# +# Managed is authoritative because it is the scope an organization uses to enforce +# a policy and the one no user or project scope can override; a gate that let a +# user (or a repo) out-vote it would be exactly the policy bypass this function is +# supposed to close. The env channel sits BELOW user settings for the same reason +# it is kept at all: it is honored only in case it is ever delivered here, and for +# an UNCONFIGURED key a repo `.claude/settings.json` `env` block freely populates +# CLAUDE_PLUGIN_OPTION_* with no provenance +# (docs/conventions/hook-config-delivery/README.md fact 4), so it must never +# out-vote a value a real settings scope actually configured. +# +# RESIDUALS (accepted; see this plugin's CHANGELOG for the trust analysis): the +# USER settings file is still located from ${CLAUDE_CONFIG_DIR:-$HOME/.claude} +# rather than channel F's install anchor, and a value supplied only through a +# session `--settings` file is invisible to any on-disk read (channel F's own +# documented residual). Neither weakens the MANAGED verdict, which is +# environment-independent by construction. +_rlg_tee_enabled() { + local v + if v="$(_rlg_managed_option)"; then + [[ "$v" == "false" ]] && return 1 + return 0 + fi + if v="$(_rlg_settings_option "${CLAUDE_CONFIG_DIR:-${HOME:-}/.claude}/settings.json")"; then + [[ "$v" == "false" ]] && return 1 + return 0 + fi + case "${CLAUDE_PLUGIN_OPTION_RATE_LIMIT_GUARD_ENABLED:-}" in + "" | true) return 0 ;; + *) return 1 ;; + esac +} + +main() { + read_tee_input + + if _rlg_tee_enabled; then + tee_snapshot + fi + + if (($#)); then + # Wrapped mode: transparent passthrough. The wrapped command sees the same + # stdin bytes and owns stdout; its exit code is the wrapper's. pipefail is + # dropped for exactly this pipeline: a wrapped command that never reads + # stdin closes the pipe under printf, and pipefail would surface printf's + # SIGPIPE (141) instead of the wrapped command's own exit code. printf's + # stderr is silenced for the same case (bash prints a broken-pipe notice). + local rc + set +o pipefail + printf '%s' "$INPUT" 2>/dev/null | "$@" + rc=$? + set -o pipefail + if ! command -v jq >/dev/null 2>&1; then + printf 'rate-limit-guard: jq not found — rate-limit tee disabled (https://jqlang.org/download/)\n' + fi + exit "$rc" + fi + + # Standalone mode: minimal statusline when none was configured. + if command -v jq >/dev/null 2>&1; then + printf '%s' "$INPUT" | jq -r ' + def pct(w): ((w // {}) | if .used_percentage != null then "\(.used_percentage)%" else "-" end); + "[\(.model.display_name // "Claude")] ctx \(.context_window.used_percentage // "-")% | 5h " + + pct(.rate_limits.five_hour) + " | 7d " + pct(.rate_limits.seven_day) + ' 2>/dev/null || printf 'rate-limit-guard: waiting for session data\n' + else + printf 'rate-limit-guard: jq not found — install jq (https://jqlang.org/download/)\n' + fi + exit 0 +} -# Standalone mode: minimal statusline when none was configured. -if command -v jq >/dev/null 2>&1; then - printf '%s' "$INPUT" | jq -r ' - def pct(w): ((w // {}) | if .used_percentage != null then "\(.used_percentage)%" else "-" end); - "[\(.model.display_name // "Claude")] ctx \(.context_window.used_percentage // "-")% | 5h " - + pct(.rate_limits.five_hour) + " | 7d " + pct(.rate_limits.seven_day) - ' 2>/dev/null || printf 'rate-limit-guard: waiting for session data\n' -else - printf 'rate-limit-guard: jq not found — install jq (https://jqlang.org/download/)\n' +# Sourcing guard (the idiom already used by this repo's updater scripts): `main` +# runs only on a direct invocation, so a direct run is byte-for-byte what it was, +# while the test suite can SOURCE this file to stub _rlg_managed_settings_files — +# whose paths are fixed and root-owned by design, therefore unwritable from a +# test — and then drive the real end-to-end path through `main`. +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" fi -exit 0 diff --git a/plugins/rate-limit-guard/scripts/statusline-tee.test.sh b/plugins/rate-limit-guard/scripts/statusline-tee.test.sh index 4a5b1e64e..b2d59a08d 100755 --- a/plugins/rate-limit-guard/scripts/statusline-tee.test.sh +++ b/plugins/rate-limit-guard/scripts/statusline-tee.test.sh @@ -18,6 +18,11 @@ set -uo pipefail +# Hermeticity for the enablement-gate cases below: both of these are real inputs +# to _rlg_tee_enabled, so a developer's own exported value would otherwise decide +# what this suite proves. Every case supplies its settings through a scoped HOME. +unset CLAUDE_CONFIG_DIR CLAUDE_PLUGIN_OPTION_RATE_LIMIT_GUARD_ENABLED + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" TEE="$SCRIPT_DIR/statusline-tee.sh" @@ -389,6 +394,147 @@ else fail "stale lock directory survived" fi +# --- The enablement gate (_rlg_tee_enabled) ---------------------------------- +# Third limb of the contract: the tee's WRITE is gated by +# `rate_limit_guard_enabled`, resolved from the settings scopes Claude Code +# actually reads pluginConfigs back from (docs/conventions/hook-config-delivery, +# fact 5) rather than from an environment variable this non-hook process never +# receives. Two properties are inseparable and every case asserts BOTH: the gate +# decides whether tee_snapshot runs, and it NEVER touches the wrapped +# statusline's stdout — a gate that blanked the status line would be worse than +# the bug it closes. +# +# Managed settings live at fixed, root-owned system paths, unwritable from a test +# by design, so those cases source the wrapper (its `main` sourcing guard keeps +# that inert) to stub the path list and then drive the real end-to-end path +# through `main`. The user-scope cases are also proven black-box, unstubbed, by +# the plain `run()` case at the end. + +write_settings() { + mkdir -p "$(dirname "$1")" + printf '%s\n' "$2" >"$1" +} + +# Sourced-with-stub runner: $1 HOME, $2 managed settings file (may not exist), +# $3 stdin payload, rest = the wrapped statusline command. +gate_run() { + local home="$1" managed="$2" input="$3" + shift 3 + printf '%s' "$input" | HOME="$home" RLG_TEST_MANAGED="$managed" bash -c ' + source "$1" "$UNAME_SHIM/uname" +chmod +x "$UNAME_SHIM/uname" +if [[ -z "$(PATH="$UNAME_SHIM:$PATH" bash -c 'source "$1" + +### Options reference + +Generated from this plugin's `.claude-plugin/plugin.json`. Every option Claude Code +will prompt for when the plugin is enabled, with the environment variable each hook +reads it from. + +| Option | Type | Default | Environment variable | Description | +| --- | --- | --- | --- | --- | +| `clean_destructive_guard_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_CLEAN_DESTRUCTIVE_GUARD_ENABLED` | Session-scoped PreToolUse guard blocking destructive Bash commands while the clean skill is active | + +### How to set these + +Three supported routes, in the order most people want them: + +1. **Interactively** — Claude Code prompts for declared options when you enable the + plugin. To change them later: `/plugin configure repo-hygiene`. +2. **Headless, at install time** — repeat `--config` for each option. Replace + `` with the marketplace you installed this plugin from: + + ```shell + claude plugin install repo-hygiene@ --config clean_destructive_guard_enabled= + ``` + +3. **By hand, in settings** — add the value under `pluginConfigs` in your **user** + settings (`~/.claude/settings.json`): + + ```json + { + "pluginConfigs": { + "repo-hygiene@": { + "options": { + "clean_destructive_guard_enabled": + } + } + } + } + ``` + + Plugin option values are read from **user**, `--settings`, and managed settings + only — **not** from a project's `.claude/settings.json`. To vary behavior per + repository, enable or disable the plugin in that project's `enabledPlugins` + instead of setting an option there. + +Do not set the `CLAUDE_PLUGIN_OPTION_*` variables yourself. They are how Claude Code +hands a configured value to a hook process; the value comes from the routes above. + +### Upstream documentation + +- [User configuration](https://code.claude.com/docs/en/plugins-reference#user-configuration) — the `userConfig` schema and the `CLAUDE_PLUGIN_OPTION_` export +- [Plugin settings](https://code.claude.com/docs/en/settings#plugin-settings) — `enabledPlugins`, `extraKnownMarketplaces`, `pluginConfigs` +- [Configuration scopes](https://code.claude.com/docs/en/settings#configuration-scopes) — user vs project vs local precedence +- [Manage installed plugins](https://code.claude.com/docs/en/discover-plugins#manage-installed-plugins) — enabling, disabling, `/plugin list` + + + ## License MIT (SPDX-License-Identifier: MIT). diff --git a/plugins/ruff-format/.claude-plugin/plugin.json b/plugins/ruff-format/.claude-plugin/plugin.json index 3fbfca7bc..bbde563f9 100644 --- a/plugins/ruff-format/.claude-plugin/plugin.json +++ b/plugins/ruff-format/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "ruff-format", - "version": "0.6.3", + "version": "0.6.4", "description": "Auto-format and lint Python on edit via Ruff, only when a Ruff config governs the repo — using the consuming repo's own Ruff config.", "author": { "name": "Melodic Software", diff --git a/plugins/ruff-format/CHANGELOG.md b/plugins/ruff-format/CHANGELOG.md index 65458e427..483785009 100644 --- a/plugins/ruff-format/CHANGELOG.md +++ b/plugins/ruff-format/CHANGELOG.md @@ -3,6 +3,16 @@ All notable changes to the `ruff-format` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.6.4] + +### Changed + +- **Carries the shared hook library's new `hook::is_enabled` predicate.** `hook::check_enabled` + exits the process when a plugin is gated off, which is correct for a hook but wrong for a + caller that must keep running afterward. The resolution is now also available as a predicate + that returns instead of exiting. No behaviour of this plugin changes; the version moves so + consumers receive the updated library. + ## [0.6.3] ### Fixed diff --git a/plugins/ruff-format/README.md b/plugins/ruff-format/README.md index d04d6181c..6414de740 100644 --- a/plugins/ruff-format/README.md +++ b/plugins/ruff-format/README.md @@ -96,6 +96,63 @@ These options are user-scoped (stored in your user settings, not the project's). To turn the plugin off for a single repository, disable it in that project's `enabledPlugins` instead. + + +### Options reference + +Generated from this plugin's `.claude-plugin/plugin.json`. Every option Claude Code +will prompt for when the plugin is enabled, with the environment variable each hook +reads it from. + +| Option | Type | Default | Environment variable | Description | +| --- | --- | --- | --- | --- | +| `ruff_format_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_RUFF_FORMAT_ENABLED` | Run Ruff check --fix and format on edit of a Python file | + +### How to set these + +Three supported routes, in the order most people want them: + +1. **Interactively** — Claude Code prompts for declared options when you enable the + plugin. To change them later: `/plugin configure ruff-format`. +2. **Headless, at install time** — repeat `--config` for each option. Replace + `` with the marketplace you installed this plugin from: + + ```shell + claude plugin install ruff-format@ --config ruff_format_enabled= + ``` + +3. **By hand, in settings** — add the value under `pluginConfigs` in your **user** + settings (`~/.claude/settings.json`): + + ```json + { + "pluginConfigs": { + "ruff-format@": { + "options": { + "ruff_format_enabled": + } + } + } + } + ``` + + Plugin option values are read from **user**, `--settings`, and managed settings + only — **not** from a project's `.claude/settings.json`. To vary behavior per + repository, enable or disable the plugin in that project's `enabledPlugins` + instead of setting an option there. + +Do not set the `CLAUDE_PLUGIN_OPTION_*` variables yourself. They are how Claude Code +hands a configured value to a hook process; the value comes from the routes above. + +### Upstream documentation + +- [User configuration](https://code.claude.com/docs/en/plugins-reference#user-configuration) — the `userConfig` schema and the `CLAUDE_PLUGIN_OPTION_` export +- [Plugin settings](https://code.claude.com/docs/en/settings#plugin-settings) — `enabledPlugins`, `extraKnownMarketplaces`, `pluginConfigs` +- [Configuration scopes](https://code.claude.com/docs/en/settings#configuration-scopes) — user vs project vs local precedence +- [Manage installed plugins](https://code.claude.com/docs/en/discover-plugins#manage-installed-plugins) — enabling, disabling, `/plugin list` + + + ## License MIT (SPDX-License-Identifier: MIT). diff --git a/plugins/ruff-format/hooks/hook-utils.sh b/plugins/ruff-format/hooks/hook-utils.sh index dcb0e820f..e88315e72 100644 --- a/plugins/ruff-format/hooks/hook-utils.sh +++ b/plugins/ruff-format/hooks/hook-utils.sh @@ -16,11 +16,33 @@ readonly _HOOK_UTILS_LOADED=1 # read from the hook-process CLAUDE_PLUGIN_OPTION__ENABLED mirror. # Exits 0 (allow) if disabled. Place after source, before stdin parsing. # hook::check_enabled "MARKDOWN_FORMAT" # checks CLAUDE_PLUGIN_OPTION_MARKDOWN_FORMAT_ENABLED -hook::check_enabled() { +# +# Deliberately NOT layered with a marketplace-specific fleet switch. Claude Code +# already ships the coarse controls, and a parallel scheme here would become a +# second source of truth for the same question: +# * `--safe-mode` / `CLAUDE_CODE_SAFE_MODE` — start with every customization +# (CLAUDE.md, plugins, skills, hooks, MCP servers) disabled +# * `disableAllHooks` — disable all hooks and any custom status line +# * `claude plugin disable|enable ` — per-plugin, dependency-aware +# This helper stays scoped to the one thing it owns: the plugin's own +# `_enabled` userConfig boolean, surfaced to hook processes as the native +# `$CLAUDE_PLUGIN_OPTION_` mirror. + +# hook::is_enabled — the same check as a PREDICATE. Returns 0 when the +# plugin should run, 1 when it should not. For callers that must not terminate +# the process on a "disabled" answer. +# +# The statusline tee is exactly that caller: it is a TRANSPARENT WRAPPER around +# the user's real statusline, so exiting 0 on "disabled" would suppress the +# wrapped command's output and blank the status line. It needs to skip its own +# side effect and still pass through. +hook::is_enabled() { local var_name="CLAUDE_PLUGIN_OPTION_${1}_ENABLED" - if [[ "${!var_name:-true}" != "true" ]]; then - exit 0 - fi + [[ "${!var_name:-true}" == "true" ]] +} + +hook::check_enabled() { + hook::is_enabled "$1" || exit 0 } # --- Prerequisite visibility -------------------------------------------------- diff --git a/plugins/session-flow/README.md b/plugins/session-flow/README.md index b4038a7c4..235e73c42 100644 --- a/plugins/session-flow/README.md +++ b/plugins/session-flow/README.md @@ -316,3 +316,66 @@ network-free (retro and running-retro use the same stdlib-only Python 3.10+ pars parser, and `reconcile` reads them read-only and mutates only the in-session task ledger); `continue-in-background` spawns a local `claude --bg` process, a new Claude Code session with ordinary session network access, but the skill itself performs no egress. + + + +### Options reference + +Generated from this plugin's `.claude-plugin/plugin.json`. Every option Claude Code +will prompt for when the plugin is enabled, with the environment variable each hook +reads it from. + +| Option | Type | Default | Environment variable | Description | +| --- | --- | --- | --- | --- | +| `observer_enabled` | boolean | `false` | `CLAUDE_PLUGIN_OPTION_OBSERVER_ENABLED` | Opt in to the SessionStart hook that arms the detached running-retro observer for every real interactive session. Default off: installing session-flow changes no behavior until this is enabled. The manual `/session-flow:running-retro arm` action works regardless of this toggle. | +| `observer_analysis_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_OBSERVER_ANALYSIS_ENABLED` | When armed, run a headless post-session running-retro checkpoint after the observer detects the session ended (mtime-idle), writing the findings to the running-retro ledger. Off = the observer only distills observations and retains them under its plugin work dir for manual inspection; it does not analyze or write the ledger (no per-session Claude spend, no automatic in-session consumer). | +| `observer_analysis_model` | string | `"claude-haiku-4-5"` | `CLAUDE_PLUGIN_OPTION_OBSERVER_ANALYSIS_MODEL` | Model id for the headless post-end analysis run (the dominant cost lever). Defaults to the cheapest active tier; pin a different id to trade cost for depth. | +| `observer_analysis_bare` | boolean | `false` | `CLAUDE_PLUGIN_OPTION_OBSERVER_ANALYSIS_BARE` | Drop auto-discovery (a further cost lever) on the analysis run. Off by default because --bare fails on OAuth-login installs (the run reports 'Not logged in'); enable only where auth is an env-var API key that survives it. See reference/observer.md. | +| `observer_idle_seconds` | number | `900` | `CLAUDE_PLUGIN_OPTION_OBSERVER_IDLE_SECONDS` | How long the transcript must stop growing before the observer treats the session as ended. Keep it above the longest expected single turn (large fan-outs, long builds) or a mid-turn pause will be misread as end and fire analysis on a partial transcript. | +| `observer_poll_seconds` | number
*min 1* | `5` | `CLAUDE_PLUGIN_OPTION_OBSERVER_POLL_SECONDS` | How often the observer re-reads the transcript to distill new observations and re-check the mtime-idle threshold. Lower costs more wakeups for a faster end-detection; raise it on a busy machine. The idle threshold, not this, decides when the session is over. Bounded below at 1: 0 spins the detached observer continuously, and a negative value raises at its sleep and kills it silently. | +| `observer_max_seconds` | number | `86400` | `CLAUDE_PLUGIN_OPTION_OBSERVER_MAX_SECONDS` | Absolute cap on observer lifetime. Reaching it exits WITHOUT running analysis (it is a safety valve, not an end signal); mtime-idle is the intended terminator. Default 24h. | + +### How to set these + +Three supported routes, in the order most people want them: + +1. **Interactively** — Claude Code prompts for declared options when you enable the + plugin. To change them later: `/plugin configure session-flow`. +2. **Headless, at install time** — repeat `--config` for each option. Replace + `` with the marketplace you installed this plugin from: + + ```shell + claude plugin install session-flow@ --config observer_enabled= + ``` + +3. **By hand, in settings** — add the value under `pluginConfigs` in your **user** + settings (`~/.claude/settings.json`): + + ```json + { + "pluginConfigs": { + "session-flow@": { + "options": { + "observer_enabled": + } + } + } + } + ``` + + Plugin option values are read from **user**, `--settings`, and managed settings + only — **not** from a project's `.claude/settings.json`. To vary behavior per + repository, enable or disable the plugin in that project's `enabledPlugins` + instead of setting an option there. + +Do not set the `CLAUDE_PLUGIN_OPTION_*` variables yourself. They are how Claude Code +hands a configured value to a hook process; the value comes from the routes above. + +### Upstream documentation + +- [User configuration](https://code.claude.com/docs/en/plugins-reference#user-configuration) — the `userConfig` schema and the `CLAUDE_PLUGIN_OPTION_` export +- [Plugin settings](https://code.claude.com/docs/en/settings#plugin-settings) — `enabledPlugins`, `extraKnownMarketplaces`, `pluginConfigs` +- [Configuration scopes](https://code.claude.com/docs/en/settings#configuration-scopes) — user vs project vs local precedence +- [Manage installed plugins](https://code.claude.com/docs/en/discover-plugins#manage-installed-plugins) — enabling, disabling, `/plugin list` + + diff --git a/plugins/skill-quality/README.md b/plugins/skill-quality/README.md index 714a35e3c..8e16d4f4a 100644 --- a/plugins/skill-quality/README.md +++ b/plugins/skill-quality/README.md @@ -104,3 +104,60 @@ stands alone. exits 2. - `npx` (Node) is optional; without it the markdownlint check downgrades to a warning and the other twenty-one still gate. + + + +### Options reference + +Generated from this plugin's `.claude-plugin/plugin.json`. Every option Claude Code +will prompt for when the plugin is enabled, with the environment variable each hook +reads it from. + +| Option | Type | Default | Environment variable | Description | +| --- | --- | --- | --- | --- | +| `skills_root` | directory | *(none)* | `CLAUDE_PLUGIN_OPTION_SKILLS_ROOT` | Directory holding your skills (each a subdirectory with a SKILL.md). When unset, resolves to .claude/skills under the project root. Set this only when your skills live elsewhere. | + +### How to set these + +Three supported routes, in the order most people want them: + +1. **Interactively** — Claude Code prompts for declared options when you enable the + plugin. To change them later: `/plugin configure skill-quality`. +2. **Headless, at install time** — repeat `--config` for each option. Replace + `` with the marketplace you installed this plugin from: + + ```shell + claude plugin install skill-quality@ --config skills_root= + ``` + +3. **By hand, in settings** — add the value under `pluginConfigs` in your **user** + settings (`~/.claude/settings.json`): + + ```json + { + "pluginConfigs": { + "skill-quality@": { + "options": { + "skills_root": + } + } + } + } + ``` + + Plugin option values are read from **user**, `--settings`, and managed settings + only — **not** from a project's `.claude/settings.json`. To vary behavior per + repository, enable or disable the plugin in that project's `enabledPlugins` + instead of setting an option there. + +Do not set the `CLAUDE_PLUGIN_OPTION_*` variables yourself. They are how Claude Code +hands a configured value to a hook process; the value comes from the routes above. + +### Upstream documentation + +- [User configuration](https://code.claude.com/docs/en/plugins-reference#user-configuration) — the `userConfig` schema and the `CLAUDE_PLUGIN_OPTION_` export +- [Plugin settings](https://code.claude.com/docs/en/settings#plugin-settings) — `enabledPlugins`, `extraKnownMarketplaces`, `pluginConfigs` +- [Configuration scopes](https://code.claude.com/docs/en/settings#configuration-scopes) — user vs project vs local precedence +- [Manage installed plugins](https://code.claude.com/docs/en/discover-plugins#manage-installed-plugins) — enabling, disabling, `/plugin list` + + diff --git a/plugins/source-control/.claude-plugin/plugin.json b/plugins/source-control/.claude-plugin/plugin.json index 240883c2a..506dd596f 100644 --- a/plugins/source-control/.claude-plugin/plugin.json +++ b/plugins/source-control/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "source-control", - "version": "0.51.6", + "version": "0.51.7", "description": "Git and GitHub delivery workflow: /commit (Conventional Commits + Co-Authored-By trailer via safe heredoc mechanics), /pull-request (prep, create, CI monitoring, review-comment triage, merge, CI-log fetch), /babysit-prs (self-pacing fleet loop — safe by default; opt-in worker/autopilot tiers add gate-checked merge and thread resolution behind a deterministic Python engine), /babysit-loop (the loop-lane merge lane: a standing or drain loop that invokes babysit-prs per cycle, configured through repo-scoped babysit_loop_* keys on the layered source-control.md seam, with merge authority human-only until the target repo's tracked config adopts the lane, a gate-proven C2-mechanical baseline once adopted, and standing merge-rung raises binding from the team-tracked layer only — with one named exception, where an invocation line explicitly typing both the autopilot tier keyword and the dedicated raise argument --merge c3-this-run widens that single invocation's merge authority up to C3 behind a fresh independent frontier-tier resolver, while C4-structural and C5-untrusted-provenance stay unconditionally human-merge), /worktree (create, status, cleanup, audit for parallel-session isolation), /setup (check the effective commit-subject / PR-title convention merged across its config layers and the babysit-prs config, or apply — interview the repo and write the convention config to a chosen layer), and /resolve-conflicts (intent-first merge/rebase conflict resolution with a semantic-conflict sweep — never --abort). The commit-subject / PR-title convention is configurable via a source-control.md config written by a re-runnable setup skill, layered across a ~/.claude user-global file, the tracked team file, and a gitignored .claude/source-control.local.md personal overlay merged per key; Conventional Commits is the default when no convention is declared.", "author": { "name": "Melodic Software", @@ -40,6 +40,12 @@ "description": "Block a GitHub MCP create_pull_request/update_pull_request whose PR body would fail the repository's required pr-issue-linkage check — the MCP-surface sibling of pr-body-linkage-gate, covering cloud/remote sessions that open PRs without the gh CLI. Same policy scope: enforced only in a repository that carries .github/workflows/pr-issue-linkage.yml, and only for the repository the origin remote names.", "default": true }, + "worktree_create_gate_enabled": { + "type": "boolean", + "title": "worktree-create-gate hook", + "description": "Redirect a WorktreeCreate away from Claude Code's default location, which may be inside the repository, to the configured worktree_root. The hook already reads this option and names it in its own skip message, but the option was never declared here — so Claude Code never exported CLAUDE_PLUGIN_OPTION_WORKTREE_CREATE_GATE_ENABLED, the hook's `:-true` fallback always won, and the gate could not be turned off.", + "default": true + }, "babysit_watched_owners": { "type": "string", "multiple": true, diff --git a/plugins/source-control/CHANGELOG.md b/plugins/source-control/CHANGELOG.md index ed0c285f5..d1787e7ea 100644 --- a/plugins/source-control/CHANGELOG.md +++ b/plugins/source-control/CHANGELOG.md @@ -3,6 +3,19 @@ All notable changes to the `source-control` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.51.7] + +### Fixed + +- **`worktree_create_gate_enabled` could not be turned off.** `hooks/worktree-create-gate.sh` + reads `CLAUDE_PLUGIN_OPTION_WORKTREE_CREATE_GATE_ENABLED` and names the option in its own skip + message, but the option was never declared in `.claude-plugin/plugin.json`. Claude Code exports + `CLAUDE_PLUGIN_OPTION_` only for **declared** options, so the variable was never set, the + hook's `:-true` fallback always won, and the gate ran unconditionally. Setting the option + produced no effect and no error — the failure was silent in both directions. The declaration is + now present with `default: true`, so behaviour is unchanged for anyone who does not set it, and + the documented routes for setting it now work. + ## [0.51.6] ### Changed diff --git a/plugins/source-control/README.md b/plugins/source-control/README.md index 901f4563b..a15b932cc 100644 --- a/plugins/source-control/README.md +++ b/plugins/source-control/README.md @@ -269,3 +269,93 @@ The plugin-scope finding-classification gate accepts extra posting identities vi merge authority. - Bundled scripts are read-only against the GitHub API except where the skill body documents a write. + + + +### Options reference + +Generated from this plugin's `.claude-plugin/plugin.json`. Every option Claude Code +will prompt for when the plugin is enabled, with the environment variable each hook +reads it from. + +| Option | Type | Default | Environment variable | Description | +| --- | --- | --- | --- | --- | +| `lane_instance` | string | *(none)* | `CLAUDE_PLUGIN_OPTION_LANE_INSTANCE` | Writer identity for this machine's loop-lane telemetry, per the loop-lane convention's lane-instance identity rule. It becomes the suffix of the babysit-loop telemetry sentinel marker (`source-control:babysit-loop@`), so each concurrently running lane instance owns its own comment and none can overwrite another's durable state. Must match ^\[a-z0-9\]\[a-z0-9-\]{0,31}$, be stable across restarts, and be distinct across concurrent instances; two lanes on one machine each need an explicit value. Absent: the sanitized lowercased hostname. The value appears verbatim in tracker comments — set an opaque id if a machine name should not be published in a public tracker. | +| `pr_body_linkage_gate_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_PR_BODY_LINKAGE_GATE_ENABLED` | Block a `gh pr create`/`gh pr edit` whose statically-readable PR body would fail the repository's required pr-issue-linkage check (missing a closing keyword, or a missing/empty `## Related` section). Enforced only in a repository that carries .github/workflows/pr-issue-linkage.yml; a body the hook cannot read statically always passes. | +| `pr_linkage_mcp_gate_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_PR_LINKAGE_MCP_GATE_ENABLED` | Block a GitHub MCP create_pull_request/update_pull_request whose PR body would fail the repository's required pr-issue-linkage check — the MCP-surface sibling of pr-body-linkage-gate, covering cloud/remote sessions that open PRs without the gh CLI. Same policy scope: enforced only in a repository that carries .github/workflows/pr-issue-linkage.yml, and only for the repository the origin remote names. | +| `worktree_create_gate_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_WORKTREE_CREATE_GATE_ENABLED` | Redirect a WorktreeCreate away from Claude Code's default location, which may be inside the repository, to the configured worktree_root. The hook already reads this option and names it in its own skip message, but the option was never declared here — so Claude Code never exported CLAUDE_PLUGIN_OPTION_WORKTREE_CREATE_GATE_ENABLED, the hook's `:-true` fallback always won, and the gate could not be turned off. | +| `babysit_watched_owners` | string (multiple) | *(none)* | `CLAUDE_PLUGIN_OPTION_BABYSIT_WATCHED_OWNERS` | GitHub owners (users/orgs) babysit-prs may act under. Absent: the current repo's owner is inferred per run. | +| `babysit_self_logins` | string (multiple) | *(none)* | `CLAUDE_PLUGIN_OPTION_BABYSIT_SELF_LOGINS` | Extra GitHub posting identities (e.g. a project bot account) added to your `gh api user` login — the self set babysit-prs treats as its own: self-comment suppression, same-login classification, readiness-gate classification rows, the merge-gate self-exemption, and the resolve-thread bot-only test (a self-authored reply to a bot thread no longer counts as a disqualifying human participant). Not a discovery filter — which authors' PRs the queue discovers is `--author`'s job, independent of this set. Absent: your gh login alone. | +| `babysit_intended_write_identity` | string | *(none)* | `CLAUDE_PLUGIN_OPTION_BABYSIT_INTENDED_WRITE_IDENTITY` | The single GitHub login babysit-prs's own writes are intended to land under — typically the bot posting identity. When a write the orchestrator recorded performing lands under a different `babysit_self_logins` identity (e.g. a bot-token mint failed and the write silently fell back to your personal login), the cycle status surfaces an attribution-drift material finding instead of proceeding silently. Set it to one of your self logins; a value that is not actually a posting identity would flag every write. Absent: the check is dormant. | +| `babysit_default_tier` | string | `"safe"` | `CLAUDE_PLUGIN_OPTION_BABYSIT_DEFAULT_TIER` | Tier an explicit bare /source-control:babysit-prs invocation runs: safe, worker, or autopilot. Never applies to auto-routed invocations. | +| `babysit_merge_method` | string | *(none)* | `CLAUDE_PLUGIN_OPTION_BABYSIT_MERGE_METHOD` | Merge method for gate-proven merges: merge, squash, or rebase. Absent: repo convention, then squash. | +| `babysit_autopilot_merge_tier` | boolean | `false` | `CLAUDE_PLUGIN_OPTION_BABYSIT_AUTOPILOT_MERGE_TIER` | Enable the #476 autopilot merge tier: a distinct bot account submits a genuine approving review, then the gate merges only when every criterion holds (issue-linked, lane-authored, no do-not-merge label, distinct-bot approval on the live head, no human blocking comment). Ships DISABLED; a deliberate operator opt-in. Requires babysit_lane_logins, babysit_approver_bot_logins, and babysit_merge_block_labels to be set. Absent/false: the tier does not exist and PRs go to the human merge-ready list. | +| `babysit_lane_logins` | string (multiple) | *(none)* | `CLAUDE_PLUGIN_OPTION_BABYSIT_LANE_LOGINS` | Author logins recognized as pipeline lanes for the autopilot merge tier's lane-authored criterion. Absent: the tier (when enabled) refuses fail-closed. | +| `babysit_approver_bot_logins` | string (multiple) | *(none)* | `CLAUDE_PLUGIN_OPTION_BABYSIT_APPROVER_BOT_LOGINS` | Bot logins whose approving review satisfies the autopilot merge tier's author != approver criterion. Absent: the tier (when enabled) refuses fail-closed. | +| `babysit_merge_block_labels` | string (multiple) | *(none)* | `CLAUDE_PLUGIN_OPTION_BABYSIT_MERGE_BLOCK_LABELS` | Labels that veto an autopilot-merge-tier merge, e.g. do-not-merge. Absent: the tier (when enabled) refuses fail-closed. | +| `babysit_review_trigger_phrase` | string | *(none)* | `CLAUDE_PLUGIN_OPTION_BABYSIT_REVIEW_TRIGGER_PHRASE` | Comment phrase that requests an AI re-review (posted and recognized). Absent: the review-trigger module stays dormant. | +| `babysit_review_bot_logins` | string (multiple) | *(none)* | `CLAUDE_PLUGIN_OPTION_BABYSIT_REVIEW_BOT_LOGINS` | Logins of the AI review bots the trigger phrase addresses, and whose review of the live head the merge gate waits for. Absent: the review-trigger module stays dormant and the merge gate's review-settle hold stays dormant. | +| `babysit_review_settle_minutes` | string | *(none)* | `CLAUDE_PLUGIN_OPTION_BABYSIT_REVIEW_SETTLE_MINUTES` | How long after a head appears a review bot's re-review may still be in flight. The merge gate holds a head that bot has not reviewed yet until the window elapses, then stops waiting. Requires babysit_review_bot_logins; absent, the hold stays dormant. Set it above the reviewer's observed latency. | +| `babysit_review_gate_context` | string | *(none)* | `CLAUDE_PLUGIN_OPTION_BABYSIT_REVIEW_GATE_CONTEXT` | Check/status context name of the AI-review gate. Absent: gate treated as absent (degrade). | +| `babysit_ci_gateway_context` | string | *(none)* | `CLAUDE_PLUGIN_OPTION_BABYSIT_CI_GATEWAY_CONTEXT` | Check/status context name of a CI gateway check. Absent: gateway classification unused. | +| `babysit_extra_bot_logins` | string (multiple) | *(none)* | `CLAUDE_PLUGIN_OPTION_BABYSIT_EXTRA_BOT_LOGINS` | Additional logins to treat as bots when structural detection cannot identify them. Absent: structural detection only. | +| `babysit_extra_dependency_manager_logins` | string (multiple) | *(none)* | `CLAUDE_PLUGIN_OPTION_BABYSIT_EXTRA_DEPENDENCY_MANAGER_LOGINS` | Additional dependency-manager bot logins beyond the built-in dependabot/renovate set whose PRs the merge gate holds absent --allow-dependency, the same as the built-ins. Absent: built-in dependency-manager set only. | +| `babysit_approval_downgrade_logins` | string (multiple) | *(none)* | `CLAUDE_PLUGIN_OPTION_BABYSIT_APPROVAL_DOWNGRADE_LOGINS` | AI reviewer logins whose approval is surfaced as a `material` finding instead of `ignored` in the one case the structural approval-downgrade reaches: a review body carrying blocking-looking prose that still parses as an approval verdict (no CRITICAL/IMPORTANT or required-fix marker). Every bot's such approval is downgraded to non-blocking regardless; naming a login opts its own into the more-conservative `material` bucket rather than being ignored. Does not affect a review already in the APPROVED state or a plain clean approval with no blocking-looking prose — both are ignored regardless. Absent: such approvals are ignored for every bot. | +| `babysit_skip_downgrade_logins` | string (multiple) | *(none)* | `CLAUDE_PLUGIN_OPTION_BABYSIT_SKIP_DOWNGRADE_LOGINS` | AI reviewer logins whose skip/no-op review is not treated as an approval. Absent: the downgrade heuristic stays dormant. | +| `babysit_max_quiet_recheck_seconds` | number | `14400` | `CLAUDE_PLUGIN_OPTION_BABYSIT_MAX_QUIET_RECHECK_SECONDS` | Longest a quiet PR may go without a worker recheck. | +| `babysit_stuck_check_age_seconds` | number | `1800` | `CLAUDE_PLUGIN_OPTION_BABYSIT_STUCK_CHECK_AGE_SECONDS` | Minimum age before a pending non-required check under UNSTABLE is reported stuck (stuck_queued / never_settling material finding). Orphaned status contexts with no backing run are detected structurally and ignore this threshold. | +| `babysit_advisory_fix_round_cap` | number | `100` | `CLAUDE_PLUGIN_OPTION_BABYSIT_ADVISORY_FIX_ROUND_CAP` | Per-PR cap on advisory-only fix rounds (never caps blocking defects). | +| `babysit_worker_concurrency_cap` | number | `10` | `CLAUDE_PLUGIN_OPTION_BABYSIT_WORKER_CONCURRENCY_CAP` | Maximum per-PR workers dispatched concurrently in one cycle. | +| `babysit_worktree_root` | directory | *(none)* | `CLAUDE_PLUGIN_OPTION_BABYSIT_WORKTREE_ROOT` | Root directory for babysit-managed ephemeral worktrees. Absent: the worktrees/ subdirectory of the plugin data dir. | +| `worktree_root` | directory | *(none)* | `CLAUDE_PLUGIN_OPTION_WORKTREE_ROOT` | External root under which /worktree create places worktrees, as /-- — a path OUTSIDE every repository (on Windows, the same drive as the repo). Absent: the worktrees/ subdirectory of the plugin data dir, which the skill supplies explicitly rather than reading from the environment (not per-plugin in a Bash-tool subprocess). Deliberately outside the repository tree AND outside repository-discovery roots such as a ghq root, which a checkout-relative default would land inside. Never the in-repo .claude/worktrees/ default: from a worktree nested inside a checkout, a read matching a path-scoped rule's glob also loads the parent checkout's copy of that rule. | +| `worktree_stale_days` | number
*min 1* | `14` | `CLAUDE_PLUGIN_OPTION_WORKTREE_STALE_DAYS` | Days since last commit before /worktree status classifies a worktree as stale | +| `fetch_logs_max_bytes` | number
*min 1* | `52428800` | `CLAUDE_PLUGIN_OPTION_FETCH_LOGS_MAX_BYTES` | Abort a CI-log ZIP fetch larger than this | +| `branch_issue_pattern` | string | *(none)* | `CLAUDE_PLUGIN_OPTION_BRANCH_ISSUE_PATTERN` | POSIX ERE for extracting the numeric GitHub issue number from the current branch name; the LAST capture group holds it and must resolve to digits (Closes #N honors only a numeric issue). Set this for a non-default branch scheme that places the number differently, e.g. '^\[^/\]+/(\[0-9\]+)-' for 'alice/1234-slug' or '-(\[0-9\]+)$' for 'feat/add-widget-1234'. Absent: the built-in '/-' (and routine-issue-) convention. | +| `setup_inference_window` | string | `"1 year"` | `CLAUDE_PLUGIN_OPTION_SETUP_INFERENCE_WINDOW` | git log --since window /source-control:setup samples for commit-subject convention inference (any git-approxidate, e.g. '1 year', '6 months'). Absent: 1 year. | +| `setup_inference_recency_days` | number
*min 1* | `90` | `CLAUDE_PLUGIN_OPTION_SETUP_INFERENCE_RECENCY_DAYS` | Boundary for the recency split in /source-control:setup's convention-inference report — subjects newer than this many days are the 'recent' bucket, weighted as the live convention when its share diverges from the older bucket. Absent: 90. | +| `setup_inference_min_commits` | number
*min 1* | `50` | `CLAUDE_PLUGIN_OPTION_SETUP_INFERENCE_MIN_COMMITS` | Below this many classifiable subjects in the window, /source-control:setup widens inference to full history; still below it, the inference is reported low-confidence rather than authoritative. Absent: 50. | + +### How to set these + +Three supported routes, in the order most people want them: + +1. **Interactively** — Claude Code prompts for declared options when you enable the + plugin. To change them later: `/plugin configure source-control`. +2. **Headless, at install time** — repeat `--config` for each option. Replace + `` with the marketplace you installed this plugin from: + + ```shell + claude plugin install source-control@ --config lane_instance= + ``` + +3. **By hand, in settings** — add the value under `pluginConfigs` in your **user** + settings (`~/.claude/settings.json`): + + ```json + { + "pluginConfigs": { + "source-control@": { + "options": { + "lane_instance": + } + } + } + } + ``` + + Plugin option values are read from **user**, `--settings`, and managed settings + only — **not** from a project's `.claude/settings.json`. To vary behavior per + repository, enable or disable the plugin in that project's `enabledPlugins` + instead of setting an option there. + +Do not set the `CLAUDE_PLUGIN_OPTION_*` variables yourself. They are how Claude Code +hands a configured value to a hook process; the value comes from the routes above. + +### Upstream documentation + +- [User configuration](https://code.claude.com/docs/en/plugins-reference#user-configuration) — the `userConfig` schema and the `CLAUDE_PLUGIN_OPTION_` export +- [Plugin settings](https://code.claude.com/docs/en/settings#plugin-settings) — `enabledPlugins`, `extraKnownMarketplaces`, `pluginConfigs` +- [Configuration scopes](https://code.claude.com/docs/en/settings#configuration-scopes) — user vs project vs local precedence +- [Manage installed plugins](https://code.claude.com/docs/en/discover-plugins#manage-installed-plugins) — enabling, disabling, `/plugin list` + + diff --git a/plugins/source-control/hooks/hook-utils.sh b/plugins/source-control/hooks/hook-utils.sh index dcb0e820f..e88315e72 100644 --- a/plugins/source-control/hooks/hook-utils.sh +++ b/plugins/source-control/hooks/hook-utils.sh @@ -16,11 +16,33 @@ readonly _HOOK_UTILS_LOADED=1 # read from the hook-process CLAUDE_PLUGIN_OPTION__ENABLED mirror. # Exits 0 (allow) if disabled. Place after source, before stdin parsing. # hook::check_enabled "MARKDOWN_FORMAT" # checks CLAUDE_PLUGIN_OPTION_MARKDOWN_FORMAT_ENABLED -hook::check_enabled() { +# +# Deliberately NOT layered with a marketplace-specific fleet switch. Claude Code +# already ships the coarse controls, and a parallel scheme here would become a +# second source of truth for the same question: +# * `--safe-mode` / `CLAUDE_CODE_SAFE_MODE` — start with every customization +# (CLAUDE.md, plugins, skills, hooks, MCP servers) disabled +# * `disableAllHooks` — disable all hooks and any custom status line +# * `claude plugin disable|enable ` — per-plugin, dependency-aware +# This helper stays scoped to the one thing it owns: the plugin's own +# `_enabled` userConfig boolean, surfaced to hook processes as the native +# `$CLAUDE_PLUGIN_OPTION_` mirror. + +# hook::is_enabled — the same check as a PREDICATE. Returns 0 when the +# plugin should run, 1 when it should not. For callers that must not terminate +# the process on a "disabled" answer. +# +# The statusline tee is exactly that caller: it is a TRANSPARENT WRAPPER around +# the user's real statusline, so exiting 0 on "disabled" would suppress the +# wrapped command's output and blank the status line. It needs to skip its own +# side effect and still pass through. +hook::is_enabled() { local var_name="CLAUDE_PLUGIN_OPTION_${1}_ENABLED" - if [[ "${!var_name:-true}" != "true" ]]; then - exit 0 - fi + [[ "${!var_name:-true}" == "true" ]] +} + +hook::check_enabled() { + hook::is_enabled "$1" || exit 0 } # --- Prerequisite visibility -------------------------------------------------- diff --git a/plugins/typos-format/.claude-plugin/plugin.json b/plugins/typos-format/.claude-plugin/plugin.json index 03ced2e7d..a839f3066 100644 --- a/plugins/typos-format/.claude-plugin/plugin.json +++ b/plugins/typos-format/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "typos-format", - "version": "0.6.4", + "version": "0.6.5", "description": "Spell-check on edit via typos-cli, unconditionally — report-only by default, honoring the consuming repo's own typos configuration when one is present.", "author": { "name": "Melodic Software", diff --git a/plugins/typos-format/CHANGELOG.md b/plugins/typos-format/CHANGELOG.md index ab537667d..c82ad34ff 100644 --- a/plugins/typos-format/CHANGELOG.md +++ b/plugins/typos-format/CHANGELOG.md @@ -3,6 +3,16 @@ All notable changes to the `typos-format` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.6.5] + +### Changed + +- **Carries the shared hook library's new `hook::is_enabled` predicate.** `hook::check_enabled` + exits the process when a plugin is gated off, which is correct for a hook but wrong for a + caller that must keep running afterward. The resolution is now also available as a predicate + that returns instead of exiting. No behaviour of this plugin changes; the version moves so + consumers receive the updated library. + ## [0.6.4] ### Changed diff --git a/plugins/typos-format/README.md b/plugins/typos-format/README.md index b6a6e66f1..b9205c991 100644 --- a/plugins/typos-format/README.md +++ b/plugins/typos-format/README.md @@ -115,6 +115,64 @@ These options are user-scoped (stored in your user settings, not the project's). To turn the plugin off for a single repository, disable it in that project's `enabledPlugins` instead. + + +### Options reference + +Generated from this plugin's `.claude-plugin/plugin.json`. Every option Claude Code +will prompt for when the plugin is enabled, with the environment variable each hook +reads it from. + +| Option | Type | Default | Environment variable | Description | +| --- | --- | --- | --- | --- | +| `typos_format_enabled` | boolean | `true` | `CLAUDE_PLUGIN_OPTION_TYPOS_FORMAT_ENABLED` | Spell-check on edit of any file, unconditionally (report-only unless typos_format_write_changes is on) | +| `typos_format_write_changes` | boolean | `false` | `CLAUDE_PLUGIN_OPTION_TYPOS_FORMAT_WRITE_CHANGES` | Rewrite the file in place. Off by default: findings are reported without modifying the file. Turning this on accepts last-writer-wins ordering with any sibling formatter hook that rewrites the same file. | + +### How to set these + +Three supported routes, in the order most people want them: + +1. **Interactively** — Claude Code prompts for declared options when you enable the + plugin. To change them later: `/plugin configure typos-format`. +2. **Headless, at install time** — repeat `--config` for each option. Replace + `` with the marketplace you installed this plugin from: + + ```shell + claude plugin install typos-format@ --config typos_format_enabled= + ``` + +3. **By hand, in settings** — add the value under `pluginConfigs` in your **user** + settings (`~/.claude/settings.json`): + + ```json + { + "pluginConfigs": { + "typos-format@": { + "options": { + "typos_format_enabled": + } + } + } + } + ``` + + Plugin option values are read from **user**, `--settings`, and managed settings + only — **not** from a project's `.claude/settings.json`. To vary behavior per + repository, enable or disable the plugin in that project's `enabledPlugins` + instead of setting an option there. + +Do not set the `CLAUDE_PLUGIN_OPTION_*` variables yourself. They are how Claude Code +hands a configured value to a hook process; the value comes from the routes above. + +### Upstream documentation + +- [User configuration](https://code.claude.com/docs/en/plugins-reference#user-configuration) — the `userConfig` schema and the `CLAUDE_PLUGIN_OPTION_` export +- [Plugin settings](https://code.claude.com/docs/en/settings#plugin-settings) — `enabledPlugins`, `extraKnownMarketplaces`, `pluginConfigs` +- [Configuration scopes](https://code.claude.com/docs/en/settings#configuration-scopes) — user vs project vs local precedence +- [Manage installed plugins](https://code.claude.com/docs/en/discover-plugins#manage-installed-plugins) — enabling, disabling, `/plugin list` + + + ## License MIT (SPDX-License-Identifier: MIT). diff --git a/plugins/typos-format/hooks/hook-utils.sh b/plugins/typos-format/hooks/hook-utils.sh index dcb0e820f..e88315e72 100644 --- a/plugins/typos-format/hooks/hook-utils.sh +++ b/plugins/typos-format/hooks/hook-utils.sh @@ -16,11 +16,33 @@ readonly _HOOK_UTILS_LOADED=1 # read from the hook-process CLAUDE_PLUGIN_OPTION__ENABLED mirror. # Exits 0 (allow) if disabled. Place after source, before stdin parsing. # hook::check_enabled "MARKDOWN_FORMAT" # checks CLAUDE_PLUGIN_OPTION_MARKDOWN_FORMAT_ENABLED -hook::check_enabled() { +# +# Deliberately NOT layered with a marketplace-specific fleet switch. Claude Code +# already ships the coarse controls, and a parallel scheme here would become a +# second source of truth for the same question: +# * `--safe-mode` / `CLAUDE_CODE_SAFE_MODE` — start with every customization +# (CLAUDE.md, plugins, skills, hooks, MCP servers) disabled +# * `disableAllHooks` — disable all hooks and any custom status line +# * `claude plugin disable|enable ` — per-plugin, dependency-aware +# This helper stays scoped to the one thing it owns: the plugin's own +# `_enabled` userConfig boolean, surfaced to hook processes as the native +# `$CLAUDE_PLUGIN_OPTION_` mirror. + +# hook::is_enabled — the same check as a PREDICATE. Returns 0 when the +# plugin should run, 1 when it should not. For callers that must not terminate +# the process on a "disabled" answer. +# +# The statusline tee is exactly that caller: it is a TRANSPARENT WRAPPER around +# the user's real statusline, so exiting 0 on "disabled" would suppress the +# wrapped command's output and blank the status line. It needs to skip its own +# side effect and still pass through. +hook::is_enabled() { local var_name="CLAUDE_PLUGIN_OPTION_${1}_ENABLED" - if [[ "${!var_name:-true}" != "true" ]]; then - exit 0 - fi + [[ "${!var_name:-true}" == "true" ]] +} + +hook::check_enabled() { + hook::is_enabled "$1" || exit 0 } # --- Prerequisite visibility -------------------------------------------------- diff --git a/plugins/visualization/README.md b/plugins/visualization/README.md index 676f8438d..310c23bfd 100644 --- a/plugins/visualization/README.md +++ b/plugins/visualization/README.md @@ -85,3 +85,60 @@ its own. maintained security posture (a self-hosted AntV deployment is the current candidate) — until then the skill relies only on native rendering surfaces and the presence-gated craft capabilities. + + + +### Options reference + +Generated from this plugin's `.claude-plugin/plugin.json`. Every option Claude Code +will prompt for when the plugin is enabled, with the environment variable each hook +reads it from. + +| Option | Type | Default | Environment variable | Description | +| --- | --- | --- | --- | --- | +| `medium` | string | `"auto"` | `CLAUDE_PLUGIN_OPTION_MEDIUM` | Preferred delivery medium when the skill auto-selects. One of: 'auto' (decide by content and available surfaces), 'terminal' (always render inline, degrading richer forms to their best terminal approximation), 'file' (render richer forms as a self-contained local HTML file, never published off the machine), 'artifact' (prefer a published Artifact when that surface is available, else fall back to a local HTML file, else terminal). An unrecognized value is reported and treated as 'auto'. | + +### How to set these + +Three supported routes, in the order most people want them: + +1. **Interactively** — Claude Code prompts for declared options when you enable the + plugin. To change them later: `/plugin configure visualization`. +2. **Headless, at install time** — repeat `--config` for each option. Replace + `` with the marketplace you installed this plugin from: + + ```shell + claude plugin install visualization@ --config medium= + ``` + +3. **By hand, in settings** — add the value under `pluginConfigs` in your **user** + settings (`~/.claude/settings.json`): + + ```json + { + "pluginConfigs": { + "visualization@": { + "options": { + "medium": + } + } + } + } + ``` + + Plugin option values are read from **user**, `--settings`, and managed settings + only — **not** from a project's `.claude/settings.json`. To vary behavior per + repository, enable or disable the plugin in that project's `enabledPlugins` + instead of setting an option there. + +Do not set the `CLAUDE_PLUGIN_OPTION_*` variables yourself. They are how Claude Code +hands a configured value to a hook process; the value comes from the routes above. + +### Upstream documentation + +- [User configuration](https://code.claude.com/docs/en/plugins-reference#user-configuration) — the `userConfig` schema and the `CLAUDE_PLUGIN_OPTION_` export +- [Plugin settings](https://code.claude.com/docs/en/settings#plugin-settings) — `enabledPlugins`, `extraKnownMarketplaces`, `pluginConfigs` +- [Configuration scopes](https://code.claude.com/docs/en/settings#configuration-scopes) — user vs project vs local precedence +- [Manage installed plugins](https://code.claude.com/docs/en/discover-plugins#manage-installed-plugins) — enabling, disabling, `/plugin list` + + diff --git a/plugins/work-items/README.md b/plugins/work-items/README.md index f4c21274f..f91f30057 100644 --- a/plugins/work-items/README.md +++ b/plugins/work-items/README.md @@ -142,6 +142,69 @@ and its own `CLAUDE.md` / rules for write-identity policy (e.g. routing tracker writes through a bot wrapper) and development workflow. The skills degrade gracefully when any of these are absent. + + +### Options reference + +Generated from this plugin's `.claude-plugin/plugin.json`. Every option Claude Code +will prompt for when the plugin is enabled, with the environment variable each hook +reads it from. + +| Option | Type | Default | Environment variable | Description | +| --- | --- | --- | --- | --- | +| `lane_instance` | string | *(none)* | `CLAUDE_PLUGIN_OPTION_LANE_INSTANCE` | Writer identity for this machine's loop-lane telemetry, per the loop-lane convention's lane-instance identity rule. It becomes the suffix of the lane's telemetry sentinel marker (`work-items:work-loop@`), so each concurrently running lane instance owns its own comment and none can overwrite another's durable state — including first_drain_complete, whose loss would end one machine's earn-trust ratification gate because a different machine finished a drain. Must match ^\[a-z0-9\]\[a-z0-9-\]{0,31}$, be stable across restarts, and be distinct across concurrent instances; two lanes on one machine each need an explicit value. Absent: the sanitized lowercased hostname. The value appears verbatim in tracker comments — set an opaque id if a machine name should not be published in a public tracker. | +| `work_dispatch_concurrency_cap` | number
*min 1* | *(none)* | `CLAUDE_PLUGIN_OPTION_WORK_DISPATCH_CONCURRENCY_CAP` | Maximum concurrent dispatch waves /work-items:work's autonomous execute step allows per invocation (it runs exactly one item per invocation). Give a whole number of waves; a fractional value is floored to whole waves since a wave is discrete. When set, /work-items:work threads it into /implementation:implement-dispatch as that skill's --wave-cap ceiling. Leave unset to let implement-dispatch apply its own internal 3-5 wave default — this key declares no default, so an unset value stays distinguishable from a configured one (which a declared default would collapse into a hard cap). | +| `work_loop_item_cap_start` | number
*min 1* | `2` | `CLAUDE_PLUGIN_OPTION_WORK_LOOP_ITEM_CAP_START` | Where the work-loop lane's adaptive per-cycle item cap starts. The cap ramps up by one after three consecutive clean items (never while a rate-limit warning is latched) and drops by one on any dirty item; enforcement is the loop body's own arithmetic. | +| `work_loop_item_cap_ceiling` | number
*min 1* | `3` | `CLAUDE_PLUGIN_OPTION_WORK_LOOP_ITEM_CAP_CEILING` | Upper bound the work-loop lane's adaptive item cap can ramp to for non-frontier-tier items. Frontier-tier items are bounded separately by work_loop_frontier_item_cap_ceiling. | +| `work_loop_item_cap_floor` | number
*min 1* | `1` | `CLAUDE_PLUGIN_OPTION_WORK_LOOP_ITEM_CAP_FLOOR` | Lower bound the work-loop lane's adaptive item cap can drop to on dirty items. | +| `work_loop_frontier_item_cap_ceiling` | number
*min 1* | `2` | `CLAUDE_PLUGIN_OPTION_WORK_LOOP_FRONTIER_ITEM_CAP_CEILING` | Quota guard for frontier-capability-tier items in the work-loop lane: such items run at concurrency 1 and their adaptive cap is bounded by this ceiling instead of the general one. Keep it at or below work_loop_item_cap_ceiling. The frontier tier is read from the item body, which any item author can write, so a frontier ceiling above the general one would let a body claim buy higher throughput; the lane detects that inversion and ignores this ceiling, bounding the item by the general one instead. The manifest cannot enforce the ordering — userConfig min/max are static bounds with no cross-key validation. | +| `work_loop_no_progress_threshold` | number
*min 1* | `3` | `CLAUDE_PLUGIN_OPTION_WORK_LOOP_NO_PROGRESS_THRESHOLD` | Consecutive no-progress cycles (actionable work in view, no item advanced and no PR opened) before the work-loop lane raises its stall escalation. The lane escalates and keeps looping; it never stops on a stall. Idle cycles with nothing actionable neither count nor reset. | + +### How to set these + +Three supported routes, in the order most people want them: + +1. **Interactively** — Claude Code prompts for declared options when you enable the + plugin. To change them later: `/plugin configure work-items`. +2. **Headless, at install time** — repeat `--config` for each option. Replace + `` with the marketplace you installed this plugin from: + + ```shell + claude plugin install work-items@ --config lane_instance= + ``` + +3. **By hand, in settings** — add the value under `pluginConfigs` in your **user** + settings (`~/.claude/settings.json`): + + ```json + { + "pluginConfigs": { + "work-items@": { + "options": { + "lane_instance": + } + } + } + } + ``` + + Plugin option values are read from **user**, `--settings`, and managed settings + only — **not** from a project's `.claude/settings.json`. To vary behavior per + repository, enable or disable the plugin in that project's `enabledPlugins` + instead of setting an option there. + +Do not set the `CLAUDE_PLUGIN_OPTION_*` variables yourself. They are how Claude Code +hands a configured value to a hook process; the value comes from the routes above. + +### Upstream documentation + +- [User configuration](https://code.claude.com/docs/en/plugins-reference#user-configuration) — the `userConfig` schema and the `CLAUDE_PLUGIN_OPTION_` export +- [Plugin settings](https://code.claude.com/docs/en/settings#plugin-settings) — `enabledPlugins`, `extraKnownMarketplaces`, `pluginConfigs` +- [Configuration scopes](https://code.claude.com/docs/en/settings#configuration-scopes) — user vs project vs local precedence +- [Manage installed plugins](https://code.claude.com/docs/en/discover-plugins#manage-installed-plugins) — enabling, disabling, `/plugin list` + + + ## License MIT (SPDX-License-Identifier: MIT). diff --git a/scripts/sync-plugin-options-docs.py b/scripts/sync-plugin-options-docs.py new file mode 100755 index 000000000..f4720a6c5 --- /dev/null +++ b/scripts/sync-plugin-options-docs.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 +"""Generate the options reference block in each plugin README from its manifest. + + scripts/sync-plugin-options-docs.py rewrite every plugin README + scripts/sync-plugin-options-docs.py --check fail if any README is stale + +SINGLE SOURCE OF TRUTH: `plugins//.claude-plugin/plugin.json` -> `userConfig`. +The block between the markers below is GENERATED. Never hand-edit it: add or change +the option in the manifest and re-run this script. CI runs `--check` and rejects drift, +the same contract `scripts/sync-hook-utils.sh` uses for the shared hook library. + +Why a generated block rather than prose alone: a hand-written Configuration section +carries nuance a generator cannot (see plugins/actionlint/README.md, which explains the +stdin timeout's slicing behavior). That prose is preserved untouched. What it cannot +guarantee is COMPLETENESS and FRESHNESS -- an option added to the manifest six months +from now is silently undocumented. The generated block guarantees both; the prose keeps +the nuance. They sit next to each other in the plugin's own folder so they change together. +""" + +from __future__ import annotations + +import json +import pathlib +import sys + +BEGIN = "" +END = "" + +REPO = pathlib.Path(__file__).resolve().parent.parent +PLUGINS = REPO / "plugins" + +# A placeholder, deliberately, not this repository's marketplace name. Plugin-facing docs are +# marketplace-agnostic: a plugin can be installed from a fork, a mirror, or a private catalog +# under a different name, and `plugins/github/github.test.sh` enforces that its docs never +# hardcode one. The reader substitutes whatever they installed from. +MARKETPLACE = "" + + +def env_var(key: str) -> str: + """Claude Code exports each declared option as CLAUDE_PLUGIN_OPTION_, uppercased.""" + return f"CLAUDE_PLUGIN_OPTION_{key.upper()}" + + +def render(plugin: str, marketplace: str, options: dict) -> str: + lines = [ + BEGIN, + "", + "### Options reference", + "", + "Generated from this plugin's `.claude-plugin/plugin.json`. Every option Claude Code", + "will prompt for when the plugin is enabled, with the environment variable each hook", + "reads it from.", + "", + "| Option | Type | Default | Environment variable | Description |", + "| --- | --- | --- | --- | --- |", + ] + for key, spec in options.items(): + typ = spec.get("type", "string") + # `multiple: true` means the option takes an array of that type, and the + # constraint keys bound it. Rendering only `type` made a repeated option + # indistinguishable from a scalar one -- source-control declares 10 such options + # whose hand-written prose already says "string (multiple)", so the generated + # table contradicted the prose beside it. + if spec.get("multiple"): + typ = f"{typ} (multiple)" + bounds = [f"{k} {spec[k]}" for k in ("min", "max") if k in spec] + if spec.get("required"): + bounds.insert(0, "required") + if bounds: + typ = f"{typ}
*{', '.join(str(b) for b in bounds)}*" + default = spec.get("default", "") + # MD049: this repo's markdownlint requires asterisk emphasis, not underscore. + default = "*(none)*" if default == "" else f"`{json.dumps(default)}`" + desc = " ".join(str(spec.get("description", spec.get("title", ""))).split()) + # A description is arbitrary prose from the manifest and lands inside a table + # cell. Escape the characters that would otherwise be read as markdown: a pipe + # ends the cell, and a bracketed run such as `[a-z0-9-]` in a regex reads as an + # undefined reference link (MD052). + desc = desc.replace("|", "\\|").replace("[", "\\[").replace("]", "\\]") + if spec.get("sensitive"): + desc = "**Sensitive** — stored in the OS keychain or protected credentials file. " + desc + lines.append(f"| `{key}` | {typ} | {default} | `{env_var(key)}` | {desc} |") + + first = next(iter(options)) + lines += [ + "", + "### How to set these", + "", + "Three supported routes, in the order most people want them:", + "", + "1. **Interactively** — Claude Code prompts for declared options when you enable the", + f" plugin. To change them later: `/plugin configure {plugin}`.", + f"2. **Headless, at install time** — repeat `--config` for each option. Replace", + f" `{marketplace}` with the marketplace you installed this plugin from:", + "", + " ```shell", + f" claude plugin install {plugin}@{marketplace} --config {first}=", + " ```", + "", + "3. **By hand, in settings** — add the value under `pluginConfigs` in your **user**", + " settings (`~/.claude/settings.json`):", + "", + " ```json", + " {", + ' "pluginConfigs": {', + f' "{plugin}@{marketplace}": {{', + ' "options": {', + f' "{first}": ', + " }", + " }", + " }", + " }", + " ```", + "", + " Plugin option values are read from **user**, `--settings`, and managed settings", + " only — **not** from a project's `.claude/settings.json`. To vary behavior per", + " repository, enable or disable the plugin in that project's `enabledPlugins`", + " instead of setting an option there.", + "", + "Do not set the `CLAUDE_PLUGIN_OPTION_*` variables yourself. They are how Claude Code", + "hands a configured value to a hook process; the value comes from the routes above.", + "", + "### Upstream documentation", + "", + "- [User configuration](https://code.claude.com/docs/en/plugins-reference#user-configuration) — the `userConfig` schema and the `CLAUDE_PLUGIN_OPTION_` export", + "- [Plugin settings](https://code.claude.com/docs/en/settings#plugin-settings) — `enabledPlugins`, `extraKnownMarketplaces`, `pluginConfigs`", + "- [Configuration scopes](https://code.claude.com/docs/en/settings#configuration-scopes) — user vs project vs local precedence", + "- [Manage installed plugins](https://code.claude.com/docs/en/discover-plugins#manage-installed-plugins) — enabling, disabling, `/plugin list`", + "", + END, + ] + return "\n".join(lines) + + +def splice(readme: str, block: str) -> str: + if BEGIN in readme and END in readme: + head = readme[: readme.index(BEGIN)] + tail = readme[readme.index(END) + len(END) :] + return head + block + tail + # First insertion: before the License heading when there is one, else at the end. + marker = "\n## License" + if marker in readme: + i = readme.index(marker) + return readme[:i] + "\n" + block + "\n" + readme[i:] + return readme.rstrip("\n") + "\n\n" + block + "\n" + + +def main() -> int: + check = "--check" in sys.argv + stale, wrote, skipped = [], 0, 0 + for d in sorted(PLUGINS.iterdir()): + manifest = d / ".claude-plugin" / "plugin.json" + readme = d / "README.md" + if not manifest.exists(): + continue + try: + options = json.loads(manifest.read_text(encoding="utf-8")).get("userConfig") or {} + except json.JSONDecodeError as exc: + print(f" MANIFEST UNPARSABLE: {d.name}: {exc}", file=sys.stderr) + return 2 + if not options: + # A plugin that removed its LAST option must lose its generated block too. + # Skipping here left a stale block documenting options that no longer exist, + # and --check never read the README on this branch, so the gate reported it + # as up to date -- the one path where the gate silently fails at its own job. + if readme.exists(): + current = readme.read_text(encoding="utf-8") + if BEGIN in current and END in current: + head = current[: current.index(BEGIN)] + tail = current[current.index(END) + len(END) :] + stripped = (head.rstrip("\n") + "\n" + tail.lstrip("\n")).rstrip("\n") + "\n" + if check: + stale.append(d.name) + else: + readme.write_text(stripped, encoding="utf-8", newline="\n") + wrote += 1 + print(f" removed stale block: plugins/{d.name}/README.md (0 options)") + continue + skipped += 1 + continue + if not readme.exists(): + print(f" MISSING README: {d.name} declares {len(options)} option(s)", file=sys.stderr) + stale.append(d.name) + continue + current = readme.read_text(encoding="utf-8") + updated = splice(current, render(d.name, MARKETPLACE, options)) + if updated != current: + if check: + stale.append(d.name) + else: + readme.write_text(updated, encoding="utf-8", newline="\n") + wrote += 1 + print(f" synced: plugins/{d.name}/README.md ({len(options)} options)") + + if check: + if stale: + print(f"\nSTALE options docs in {len(stale)} plugin(s): {', '.join(stale)}", file=sys.stderr) + print("Run: python scripts/sync-plugin-options-docs.py", file=sys.stderr) + return 1 + print("plugin options docs: up to date") + return 0 + print(f"\nsynced {wrote} README(s); {skipped} plugin(s) declare no options") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())