mirror of
https://github.com/anthropics/claude-plugins-official.git
synced 2026-06-15 07:06:07 -03:00
Merge pull request #2078 from anthropics/fix-1868-claude-config-dir
security-guidance: respect CLAUDE_CONFIG_DIR for plugin state files (#1868)
This commit is contained in:
commit
c40770ae5a
@ -10,15 +10,42 @@ import os
|
|||||||
import threading
|
import threading
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
def state_dir():
|
||||||
|
"""Return the absolute path of the plugin's state directory.
|
||||||
|
|
||||||
|
Resolution precedence (highest first):
|
||||||
|
1. SECURITY_WARNINGS_STATE_DIR — plugin-specific override (existing)
|
||||||
|
2. CLAUDE_CONFIG_DIR/security — CC's config-dir env var (#1868)
|
||||||
|
3. ~/.claude/security — default fallback
|
||||||
|
|
||||||
|
Empty-string env vars are treated as not-set so a misconfigured shell
|
||||||
|
(`CLAUDE_CONFIG_DIR=` with no value) doesn't silently write to
|
||||||
|
/security at the filesystem root.
|
||||||
|
|
||||||
|
Returns a fully-expanded absolute path (no literal `~`) so subprocess
|
||||||
|
callers can pass it through to code that doesn't re-expand tildes.
|
||||||
|
|
||||||
|
Called per-invocation rather than cached at import time so test
|
||||||
|
monkeypatches of the env vars take effect — the plugin's hooks each
|
||||||
|
run as fresh subprocesses in production, so the per-call cost is
|
||||||
|
negligible compared to subprocess spawn.
|
||||||
|
"""
|
||||||
|
explicit = os.environ.get("SECURITY_WARNINGS_STATE_DIR")
|
||||||
|
if explicit:
|
||||||
|
return os.path.expanduser(explicit)
|
||||||
|
cc_config = os.environ.get("CLAUDE_CONFIG_DIR")
|
||||||
|
if cc_config:
|
||||||
|
return os.path.expanduser(os.path.join(cc_config, "security"))
|
||||||
|
return os.path.expanduser("~/.claude/security")
|
||||||
|
|
||||||
|
|
||||||
# Debug log file. Lives under the plugin state dir (default ~/.claude/security/)
|
# Debug log file. Lives under the plugin state dir (default ~/.claude/security/)
|
||||||
# rather than /tmp because /tmp is world-writable on multi-user hosts (TOCTOU /
|
# rather than /tmp because /tmp is world-writable on multi-user hosts (TOCTOU /
|
||||||
# symlink-attack surface, cross-user log leakage). Overridable per-process via
|
# symlink-attack surface, cross-user log leakage). Overridable per-process via
|
||||||
# SECURITY_GUIDANCE_DEBUG_LOG, or per-state-dir via SECURITY_WARNINGS_STATE_DIR.
|
# SECURITY_GUIDANCE_DEBUG_LOG, or per-state-dir via SECURITY_WARNINGS_STATE_DIR
|
||||||
_DEFAULT_STATE_DIR = os.path.expanduser(
|
# (plugin-specific override) or CLAUDE_CONFIG_DIR (CC-wide config dir, #1868).
|
||||||
os.environ.get("SECURITY_WARNINGS_STATE_DIR") or "~/.claude/security"
|
|
||||||
)
|
|
||||||
DEBUG_LOG_FILE = os.environ.get("SECURITY_GUIDANCE_DEBUG_LOG") or os.path.join(
|
DEBUG_LOG_FILE = os.environ.get("SECURITY_GUIDANCE_DEBUG_LOG") or os.path.join(
|
||||||
_DEFAULT_STATE_DIR, "log.txt"
|
state_dir(), "log.txt"
|
||||||
)
|
)
|
||||||
# Cap the debug log so parallel-worker fleets don't fill disk. When the active
|
# Cap the debug log so parallel-worker fleets don't fill disk. When the active
|
||||||
# file exceeds this it's atomically rotated to <file>.1 (overwriting any prior
|
# file exceeds this it's atomically rotated to <file>.1 (overwriting any prior
|
||||||
|
|||||||
@ -23,6 +23,12 @@ import sys
|
|||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Shared state-dir resolver: SECURITY_WARNINGS_STATE_DIR → CLAUDE_CONFIG_DIR/security
|
||||||
|
# → ~/.claude/security. See _base.state_dir for resolution precedence. Re-aliased
|
||||||
|
# here to match the existing local name (state_dir was already a local var in
|
||||||
|
# main() and _maybe_emit_user_notice).
|
||||||
|
from _base import state_dir as _resolve_state_dir
|
||||||
|
|
||||||
# Outcome codes for the sdk_bootstrap metric. Values are stable for telemetry.
|
# Outcome codes for the sdk_bootstrap metric. Values are stable for telemetry.
|
||||||
NOOP_SYSTEM = 0 # claude_agent_sdk already importable in system python
|
NOOP_SYSTEM = 0 # claude_agent_sdk already importable in system python
|
||||||
NOOP_VENV = 1 # venv already built and SDK imports from it
|
NOOP_VENV = 1 # venv already built and SDK imports from it
|
||||||
@ -90,10 +96,7 @@ def main() -> tuple[int, str, str]:
|
|||||||
if _sdk_on_syspath():
|
if _sdk_on_syspath():
|
||||||
return NOOP_SYSTEM, "", ""
|
return NOOP_SYSTEM, "", ""
|
||||||
|
|
||||||
state_dir = Path(
|
state_dir = Path(_resolve_state_dir())
|
||||||
os.environ.get("SECURITY_WARNINGS_STATE_DIR")
|
|
||||||
or os.path.expanduser("~/.claude/security")
|
|
||||||
)
|
|
||||||
venv = state_dir / "agent-sdk-venv"
|
venv = state_dir / "agent-sdk-venv"
|
||||||
# Windows venvs put the interpreter at Scripts\python.exe; POSIX uses bin/python.
|
# Windows venvs put the interpreter at Scripts\python.exe; POSIX uses bin/python.
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
@ -239,10 +242,7 @@ def _maybe_emit_user_notice(outcome: int, pv: int) -> str | None:
|
|||||||
if outcome != HOOK_PY_INCOMPATIBLE:
|
if outcome != HOOK_PY_INCOMPATIBLE:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
state_dir = Path(
|
state_dir = Path(_resolve_state_dir())
|
||||||
os.environ.get("SECURITY_WARNINGS_STATE_DIR")
|
|
||||||
or os.path.expanduser("~/.claude/security")
|
|
||||||
)
|
|
||||||
marker = state_dir / f".agentic_unavailable_notice_v{pv or 0}"
|
marker = state_dir / f".agentic_unavailable_notice_v{pv or 0}"
|
||||||
if marker.exists():
|
if marker.exists():
|
||||||
return None
|
return None
|
||||||
|
|||||||
@ -27,7 +27,7 @@ from typing import Optional, Tuple, Dict, Any, List
|
|||||||
|
|
||||||
import extensibility
|
import extensibility
|
||||||
import review_api
|
import review_api
|
||||||
from _base import debug_log, _record_usage, _PV, PROVENANCE_TAG # noqa: F401
|
from _base import debug_log, _record_usage, _PV, PROVENANCE_TAG, state_dir as _resolve_state_dir # noqa: F401
|
||||||
from session_state import with_locked_state
|
from session_state import with_locked_state
|
||||||
|
|
||||||
|
|
||||||
@ -355,10 +355,7 @@ def _call_claude_via_sdk(prompt, output_schema, *, max_tokens=16000, model=None)
|
|||||||
# Try the venv ensure_agent_sdk.py builds. Same fallback logic as
|
# Try the venv ensure_agent_sdk.py builds. Same fallback logic as
|
||||||
# agentic_review() — duplicated here so the 3P path doesn't require
|
# agentic_review() — duplicated here so the 3P path doesn't require
|
||||||
# the agentic path to have run first.
|
# the agentic path to have run first.
|
||||||
_state_dir = os.environ.get(
|
_state_dir = _resolve_state_dir()
|
||||||
"SECURITY_WARNINGS_STATE_DIR",
|
|
||||||
os.path.expanduser("~/.claude/security"),
|
|
||||||
)
|
|
||||||
_inject_agent_sdk_venv_into_syspath(_state_dir)
|
_inject_agent_sdk_venv_into_syspath(_state_dir)
|
||||||
try:
|
try:
|
||||||
import asyncio as _asyncio # noqa: F811
|
import asyncio as _asyncio # noqa: F811
|
||||||
@ -1145,10 +1142,7 @@ def agentic_review(
|
|||||||
# ~/.claude/security/ with the SDK installed; try that as a fallback
|
# ~/.claude/security/ with the SDK installed; try that as a fallback
|
||||||
# before giving up. The system import is attempted first so users
|
# before giving up. The system import is attempted first so users
|
||||||
# who DO have it never touch the venv.
|
# who DO have it never touch the venv.
|
||||||
_state_dir = os.environ.get(
|
_state_dir = _resolve_state_dir()
|
||||||
"SECURITY_WARNINGS_STATE_DIR",
|
|
||||||
os.path.expanduser("~/.claude/security"),
|
|
||||||
)
|
|
||||||
_venv_tried = _inject_agent_sdk_venv_into_syspath(_state_dir)
|
_venv_tried = _inject_agent_sdk_venv_into_syspath(_state_dir)
|
||||||
try:
|
try:
|
||||||
import asyncio as _asyncio # noqa: F811
|
import asyncio as _asyncio # noqa: F811
|
||||||
|
|||||||
@ -82,6 +82,7 @@ from _base import ( # noqa: E402,F401
|
|||||||
PROVENANCE_TAG, PROVENANCE_BANNER,
|
PROVENANCE_TAG, PROVENANCE_BANNER,
|
||||||
_read_plugin_version_int, _PV, _USAGE, _USAGE_LOCK,
|
_read_plugin_version_int, _PV, _USAGE, _USAGE_LOCK,
|
||||||
_PRICE_PER_MTOK, _PRICE_DEFAULT, _record_usage, _usage_metrics,
|
_PRICE_PER_MTOK, _PRICE_DEFAULT, _record_usage, _usage_metrics,
|
||||||
|
state_dir as _resolve_state_dir,
|
||||||
)
|
)
|
||||||
import extensibility # noqa: E402
|
import extensibility # noqa: E402
|
||||||
from patterns import ( # noqa: E402,F401
|
from patterns import ( # noqa: E402,F401
|
||||||
@ -2064,10 +2065,7 @@ def handle_stop_hook(input_data):
|
|||||||
})
|
})
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
_SDK_BOOTSTRAP_THROTTLE = os.path.join(
|
_SDK_BOOTSTRAP_THROTTLE = os.path.join(_resolve_state_dir(), ".sdk_bootstrap_spawned")
|
||||||
os.environ.get("SECURITY_WARNINGS_STATE_DIR")
|
|
||||||
or os.path.expanduser("~/.claude/security"),
|
|
||||||
".sdk_bootstrap_spawned")
|
|
||||||
|
|
||||||
def _maybe_bootstrap_agent_sdk_async():
|
def _maybe_bootstrap_agent_sdk_async():
|
||||||
"""Fire-and-forget SDK bootstrap, for remote-pod environments.
|
"""Fire-and-forget SDK bootstrap, for remote-pod environments.
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import os
|
|||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from _base import debug_log
|
from _base import debug_log, state_dir as _state_dir
|
||||||
|
|
||||||
|
|
||||||
def _state_key(session_id):
|
def _state_key(session_id):
|
||||||
@ -36,20 +36,20 @@ def _state_key(session_id):
|
|||||||
|
|
||||||
def get_state_file(session_id):
|
def get_state_file(session_id):
|
||||||
"""Get session-specific state file path."""
|
"""Get session-specific state file path."""
|
||||||
state_dir = os.environ.get("SECURITY_WARNINGS_STATE_DIR", os.path.expanduser("~/.claude/security"))
|
state_dir = _state_dir()
|
||||||
return os.path.join(state_dir, f"security_warnings_state_{_state_key(session_id)}.json")
|
return os.path.join(state_dir, f"security_warnings_state_{_state_key(session_id)}.json")
|
||||||
|
|
||||||
|
|
||||||
def get_lock_file(session_id):
|
def get_lock_file(session_id):
|
||||||
"""Get session-specific lock file path."""
|
"""Get session-specific lock file path."""
|
||||||
state_dir = os.environ.get("SECURITY_WARNINGS_STATE_DIR", os.path.expanduser("~/.claude/security"))
|
state_dir = _state_dir()
|
||||||
return os.path.join(state_dir, f"security_warnings_state_{_state_key(session_id)}.lock")
|
return os.path.join(state_dir, f"security_warnings_state_{_state_key(session_id)}.lock")
|
||||||
|
|
||||||
|
|
||||||
def cleanup_old_state_files():
|
def cleanup_old_state_files():
|
||||||
"""Remove state files and lock files older than 30 days."""
|
"""Remove state files and lock files older than 30 days."""
|
||||||
try:
|
try:
|
||||||
state_dir = os.environ.get("SECURITY_WARNINGS_STATE_DIR", os.path.expanduser("~/.claude/security"))
|
state_dir = _state_dir()
|
||||||
if not os.path.exists(state_dir):
|
if not os.path.exists(state_dir):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user