anthropics / anthropics/claude-plugins-official

security-guidance: user-level claude-security-guidance.md and security-patterns.yaml are never discovered when CLAUDE_CONFIG_DIR is set (#1868 fixed only for state files)

オープン
#4,826 コメント 1 件 リアクション 0 件 担当者 0 名 GitHub で見る
主要言語
Python
スター
36.3k
フォーク
4.1k
平均マージ
2日 14時間
マージ済み PR(30日)
539

説明

## Summary

The `security-guidance` plugin discovers its user-level config files at a hardcoded `~/.claude/`, ignoring `CLAUDE_CONFIG_DIR`. On any install that sets `CLAUDE_CONFIG_DIR` to something other than `~/.claude`, the user-level `claude-security-guidance.md` and `security-patterns.{yaml,json}` are silently never loaded. There is no warning, and the debug log shows no entry, so the policy simply has no effect.

This is the half of #1868 that was never fixed. Commit `0d22ba35` ("security-guidance: respect CLAUDE_CONFIG_DIR for plugin state files") scoped itself to state files and touched five modules — `_base.py`, `session_state.py`, `ensure_agent_sdk.py`, `llm.py`, `security_reminder_hook.py`. `extensibility.py` was not one of them, although it had existed since `0bde1686` three days earlier, and it has never been modified since. Its literal `~/.claude` does not contain the `security` segment the sweep looked for, so it did not match.

That commit message closes with: "Re-implementing the precedence inline risks drift — one module gets a future fix, others don't." That is exactly what happened, in the one module the sweep skipped. The two halves of the plugin now disagree with each other:

- `hooks/_base.py:33-39` reads `CLAUDE_CONFIG_DIR` and falls back to `~/.claude/security`.
- `hooks/extensibility.py:96` hardcodes `os.path.expanduser(os.path.join("~", ".claude", basename))` and reads no environment variable at all (`grep -n "environ\|getenv" extensibility.py` returns nothing).

Net effect on my machine: the plugin writes its log to `~/.claude-work/security/log.txt` (honoring `CLAUDE_CONFIG_DIR`) while looking for my policy file in `~/.claude/`, a directory that does not exist here at all.

## Environment

- Claude Code CLI: `2.1.220`
- Plugin: `security-guidance` `2.0.6` (from `claude-plugins-official`)
- Provider setup: 1P Anthropic subscription. No `ANTHROPIC_BASE_URL`, no Bedrock/Vertex env vars set.
- OS: macOS 26.5.2 (Darwin 25.5.0), arm64
- Python: 3.14.6
- `CLAUDE_CONFIG_DIR=~/.claude-work` (exported machine-wide from `~/.zshenv`; `~/.claude` does not exist)

## Repro

This runs against an isolated temporary `HOME`, so it does not touch your real config.

```bash
#!/usr/bin/env bash
set -euo pipefail

# Adjust to your install. The path itself illustrates the bug: this machine's
# config dir is ~/.claude-work, so the plugin cache lives there.
HOOKS=~/.claude-work/plugins/marketplaces/claude-plugins-official/plugins/security-guidance/hooks

H=$(mktemp -d "${TMPDIR:-/tmp}/sg-repro.XXXXXX")
mkdir -p "$H/.claude-work" "$H/.claude"
printf '# test policy\n- MARKER_POLICY_LINE\n' > "$H/.claude-work/claude-security-guidance.md"

probe() {
python3 -c "
import sys; sys.path.insert(0, '$HOOKS')
import extensibility as e
e.load_for_session(None)
print(' policy loaded:', 'MARKER_POLICY_LINE' in (e._guidance_block or ''))
"
}

echo "case 1: policy at \$CLAUDE_CONFIG_DIR/claude-security-guidance.md (no ~/.claude)"
HOME="$H" CLAUDE_CONFIG_DIR="$H/.claude-work" probe

echo "case 2: same file moved to ~/.claude/claude-security-guidance.md"
mv "$H/.claude-work/claude-security-guidance.md" "$H/.claude/claude-security-guidance.md"
HOME="$H" CLAUDE_CONFIG_DIR="$H/.claude-work" probe

rm -rf "$H"
```

**Actual:**

```
case 1: policy at $CLAUDE_CONFIG_DIR/claude-security-guidance.md (no ~/.claude)
policy loaded: False
case 2: same file moved to ~/.claude/claude-security-guidance.md
policy loaded: True
```

**Expected:** case 1 loads the policy, because `CLAUDE_CONFIG_DIR` is where this install keeps its Claude Code config.

## Log

`~/.claude-work/security/log.txt` — note that the log lives under `CLAUDE_CONFIG_DIR`, which is exactly the path `extensibility.py` declines to consult:

```
[...] Processing: hook_event=UserPromptSubmit, tool=
[...] Captured git baseline:
[...] Processing: hook_event=PostToolUse, tool=Bash
[...] Processing: hook_event=Stop, tool=
[...] Stop hook: empty review set
```

`grep extensibility log.txt` is empty across the whole session. The `extensibility: loaded N chars from ` debug line at `extensibility.py:115` can never fire for a user-level file on this install, since the only path it probes does not exist.

## Suggested fix

Mirror what `_base.py` already does, and probe both locations so existing `~/.claude` users are unaffected:

```python
def _user_config_dirs() -> List[str]:
"""User-level config dirs, in probe order. CLAUDE_CONFIG_DIR first when set;
~/.claude always, so existing installs keep working."""
dirs = []
explicit = os.environ.get("CLAUDE_CONFIG_DIR")
if explicit and explicit.strip():
dirs.append(os.path.expanduser(explicit.strip()))
default = os.path.expanduser(os.path.join("~", ".claude"))
if default not in dirs:
dirs.append(default)
return dirs

def _config_paths(cwd: Optional[str], basename: str) -> List[Tuple[str, str]]:
paths = [("User", os.path.join(d, basename)) for d in _user_config_dirs()]
if cwd:
paths.append(("Project", os.path.join(cwd, ".claude", basename)))
stem, ext = os.path.splitext(basename)
paths.append(("Project (local)", os.path.join(cwd, ".claude", f"{stem}.local{ext}")))
return paths
```

Since `_config_paths` serves both config files, this fixes `claude-security-guidance.md` and `security-patterns.{yaml,json}` together. Both remain additive-only, so the trust model in the `extensibility.py` docstring is unchanged. Empty-string handling matches `_base.state_dir`, so `CLAUDE_CONFIG_DIR=` with no value falls through to `~/.claude` instead of probing the filesystem root.

A smaller alternative, if probing two directories is unwelcome: keep one path but resolve it through the same helper `_base.py` uses, and log a debug line when a user-level file is found in neither location.

## Docs affected by the same fix

The README still documents the hardcoded literal in four places, and two of them are already wrong today, because the log path *does* honor `CLAUDE_CONFIG_DIR`:

- line 66 — `~/.claude/claude-security-guidance.md` as the user-wide location
- line 94 — "writes its own debug log to `~/.claude/security/log.txt`"
- line 102 — same log path, under Troubleshooting
- line 116 — "the relevant section of `~/.claude/security/log.txt`", under Reporting issues

The docstring at `extensibility.py:13-19` says the same thing, including "Org admins can still push files to `~/.claude/` via MDM/GPO" — which is exactly the deployment this bug breaks.

## Why this matters

The failure is silent and it disables a security control. Anyone running multiple Claude Code profiles via `CLAUDE_CONFIG_DIR` will write an org policy file, see the plugin working (pattern warnings and LLM review still run), and reasonably conclude their policy is in force when it is not. A one-line debug message on "no user-level guidance found" would also have made this diagnosable without reading the source.

コントリビューションガイド

このリポジトリのコントリビューションガイドは索引されていません

調査の方向性

Start with hooks/extensibility.py and compare its user-config path handling with hooks/_base.py, then run the supplied isolated-HOME reproduction script. Check the README references to user config and log paths. Done means claude-security-guidance.md and security-patterns.yaml/json are discovered through CLAUDE_CONFIG_DIR while existing ~/.claude and project paths continue to work, with the affected documentation corrected.

索引モデルが issue の本文から書いたものです。

評価

技術スタック
python
領域
documentation, security, tooling
issue の種類
バグ
難易度
3/5
見積もり時間
1〜2日
活発さ
静か
明瞭さ
明確に書かれている
初心者へのやさしさ
74/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。