Skip to content

disk-hygiene: skill-frontmatter guard reads CLAUDE_PLUGIN_DATA from env but never receives it -> engine lane fails closed on all platforms #376

Description

@kyle-sexton

Summary

The disk-hygiene clean skill's destructive_guard.py reads the authorized data root from the
CLAUDE_PLUGIN_DATA environment variable (os.environ.get("CLAUDE_PLUGIN_DATA") in
_is_authorized_data_root / _display_data_root). The guard is declared as a skill-frontmatter
hook
in skills/clean/SKILL.md. That environment variable is not present in the guard's
process environment, so --data-root never validates and the guard fails closed on every engine
invocation. The entire scan/preview/apply engine lane is unusable through its sanctioned Bash
lane — on all platforms, not just Windows.

Environment

  • Claude Code 2.1.215
  • Windows 11 Pro (26200); guard interpreter cpython-3.14.6 (uv-managed), Python ≥ 3.11 OK
  • disk-hygiene@melodic-software 0.4.0 (installed from main 57c4f4b)

Observed

Every guarded engine call — including the exact bundled scan shape with the hook's own absolute
interpreter — is denied with:

The hook process did not receive CLAUDE_PLUGIN_DATA, so --data-root cannot be validated and engine
calls fail closed.

${CLAUDE_PLUGIN_ROOT} does resolve in the same frontmatter hook args (the guard script path
resolves and the guard executes), so the hook fires; only the environment injection of
CLAUDE_PLUGIN_DATA is missing.

Root cause

Per the plugins reference (https://code.claude.com/docs/en/plugins-reference), the placeholders
${CLAUDE_PLUGIN_ROOT} / ${CLAUDE_PLUGIN_DATA} / ${CLAUDE_PROJECT_DIR} resolve via inline
substitution
in hook commands ("anywhere the placeholder appears"), which is a separate mechanism
from environment-variable injection into the hook subprocess. Empirically, on 2.1.215 a hook
declared in skill frontmatter gets inline substitution but does not get CLAUDE_PLUGIN_DATA
injected into its process environment. The guard depends on the env-var path exclusively, so it can
never obtain the authoritative data root.

The 0.3.0 --data-root work (and its CHANGELOG note "the runtime exports it to hook processes")
addressed the shell-tool subprocess gap but did not fix the guard side: the guard still reads the
authority from os.environ, which is empty for this hook. destructive_guard.py was not modified by
that change.

Recommended fix (preserves skill-scoping)

Pass the data root to the guard as an explicit substituted argument rather than relying on env
injection, since inline substitution is proven to work in this exact frontmatter context:

  • skills/clean/SKILL.md frontmatter hook — add the placeholder as an arg:
    hooks:
      PreToolUse:
        - matcher: "Bash|PowerShell"
          hooks:
            - type: command
              command: "python"
              args:
                - "${CLAUDE_PLUGIN_ROOT}/skills/clean/scripts/destructive_guard.py"
                - "--authorized-data-root"
                - "${CLAUDE_PLUGIN_DATA}"
  • destructive_guard.py — read the authorized root from argv (falling back to
    CLAUDE_PLUGIN_DATA env when the arg is absent, for backward compatibility), and validate the
    model-supplied --data-root against it exactly as today. The security property is unchanged: the
    authority still comes from a runtime-substituted value the model cannot forge.

Verify during implementation: confirm ${CLAUDE_PLUGIN_DATA} substitutes in skill-frontmatter
hook args the same way ${CLAUDE_PLUGIN_ROOT} already does (add it, run a scan, confirm the guard
receives the resolved absolute path).

Alternative (rejected)

Move the guard to a plugin-level hooks/hooks.json so it is unambiguously a "plugin hook" that
receives CLAUDE_PLUGIN_DATA. Downsides: the guard would then run on every Bash/PowerShell call
for the whole session (loses the intended skill-scoping), and it still depends on env injection rather
than the proven substitution path.

Impact

  • Linux: full engine (incl. apply) is unreachable through the guarded lane — the plugin's core
    value is inaccessible.
  • Windows/macOS: apply is already execution-platform-unsupported, but the deterministic scan
    (snapshot + tiered classification) is also blocked, forcing an entirely manual audit.

Repro

  1. On Windows + Claude Code 2.1.215, invoke /disk-hygiene:clean <dir>.
  2. Run the exact bundled scan shape via Bash with the hook's absolute interpreter and a correct
    --data-root.
  3. Observe the fail-closed denial citing missing CLAUDE_PLUGIN_DATA.

Metadata

Metadata

Assignees

Labels

priority: mediumReal value, no hard deadline; normal backlog flow.status: readyTriaged, unblocked, and fully specified; eligible to pick up.

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions