mirror of
https://github.com/anthropics/claude-plugins-official.git
synced 2026-06-10 18:23:36 +00:00
Compare commits
6 Commits
fix-2071-m
...
fix-1868-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d22ba3501 | ||
|
|
68a700837c | ||
|
|
3d349d40b9 | ||
|
|
6a63e35e75 | ||
|
|
12a5376e20 | ||
|
|
04127de5d1 |
@@ -10,15 +10,42 @@ import os
|
||||
import threading
|
||||
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/)
|
||||
# rather than /tmp because /tmp is world-writable on multi-user hosts (TOCTOU /
|
||||
# symlink-attack surface, cross-user log leakage). Overridable per-process via
|
||||
# SECURITY_GUIDANCE_DEBUG_LOG, or per-state-dir via SECURITY_WARNINGS_STATE_DIR.
|
||||
_DEFAULT_STATE_DIR = os.path.expanduser(
|
||||
os.environ.get("SECURITY_WARNINGS_STATE_DIR") or "~/.claude/security"
|
||||
)
|
||||
# SECURITY_GUIDANCE_DEBUG_LOG, or per-state-dir via SECURITY_WARNINGS_STATE_DIR
|
||||
# (plugin-specific override) or CLAUDE_CONFIG_DIR (CC-wide config dir, #1868).
|
||||
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
|
||||
# file exceeds this it's atomically rotated to <file>.1 (overwriting any prior
|
||||
|
||||
@@ -138,7 +138,17 @@ def restore_unreviewed_stop_state(session_id, paths, baseline_sha):
|
||||
|
||||
|
||||
def get_baseline_file_content(session_id, file_path, cwd):
|
||||
"""Get the content of a file at the baseline SHA. Returns None if unavailable."""
|
||||
"""Get the content of a file at the baseline SHA. Returns None if unavailable.
|
||||
|
||||
Decode the file content as UTF-8 with errors="replace" rather than using
|
||||
text=True: source files in user repos can be latin-1 / cp1252 / shift-jis
|
||||
/ etc., and on Windows text=True would decode via locale.getpreferredencoding()
|
||||
in strict mode and raise UnicodeDecodeError in the subprocess reader
|
||||
thread — leaving result.stdout=None and propagating AttributeError when
|
||||
the caller tries to use it. Same class as the existing migrations at
|
||||
security_reminder_hook.py:540 (reflog subjects) and :1115 (commit
|
||||
diffs); this helper was missed in that pass. See
|
||||
anthropics/claude-plugins-official#2056."""
|
||||
baseline_sha = load_baseline_sha(session_id)
|
||||
if not baseline_sha:
|
||||
return None
|
||||
@@ -151,12 +161,12 @@ def get_baseline_file_content(session_id, file_path, cwd):
|
||||
return None
|
||||
result = subprocess.run(
|
||||
[*GIT_CMD, "show", f"{baseline_sha}:{rel_path}"],
|
||||
cwd=cwd, capture_output=True, text=True, timeout=5
|
||||
cwd=cwd, capture_output=True, timeout=5
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return result.stdout
|
||||
return (result.stdout or b"").decode("utf-8", errors="replace")
|
||||
return None
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError, OSError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
@@ -173,11 +183,16 @@ def capture_git_baseline(cwd):
|
||||
and `compute_v2_review_set` subtracts that set so pre-existing untracked
|
||||
files are not reviewed as Claude-authored.
|
||||
"""
|
||||
# stdout is a SHA so text=True is safe on stdout, but a non-ASCII
|
||||
# filename in `git stash create`'s STDERR warning (e.g. a worktree
|
||||
# with `Ávila_report.txt` triggers a quotePath/locale warning) would
|
||||
# trip the stderr reader thread on Windows cp1252. Decode both streams
|
||||
# leniently for symmetry with _list_untracked. See #2056.
|
||||
try:
|
||||
# Check if HEAD exists (i.e., repo has at least one commit)
|
||||
head_check = subprocess.run(
|
||||
[*GIT_CMD, "rev-parse", "HEAD"],
|
||||
cwd=cwd, capture_output=True, text=True, timeout=5
|
||||
cwd=cwd, capture_output=True, timeout=5
|
||||
)
|
||||
if head_check.returncode != 0:
|
||||
# No commits yet — skip review rather than creating commits in the user's repo
|
||||
@@ -186,20 +201,20 @@ def capture_git_baseline(cwd):
|
||||
|
||||
result = subprocess.run(
|
||||
[*GIT_CMD, "stash", "create"],
|
||||
cwd=cwd, capture_output=True, text=True, timeout=15
|
||||
cwd=cwd, capture_output=True, timeout=15
|
||||
)
|
||||
sha = result.stdout.strip()
|
||||
sha = (result.stdout or b"").decode("utf-8", errors="replace").strip()
|
||||
if sha:
|
||||
return sha
|
||||
|
||||
# Working tree is clean — stash create returns empty. Use HEAD.
|
||||
result = subprocess.run(
|
||||
[*GIT_CMD, "rev-parse", "HEAD"],
|
||||
cwd=cwd, capture_output=True, text=True, timeout=5
|
||||
cwd=cwd, capture_output=True, timeout=5
|
||||
)
|
||||
sha = result.stdout.strip()
|
||||
sha = (result.stdout or b"").decode("utf-8", errors="replace").strip()
|
||||
return sha if sha else None
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError, OSError, ValueError) as e:
|
||||
debug_log(f"Failed to capture git baseline: {e}")
|
||||
return None
|
||||
|
||||
@@ -323,19 +338,35 @@ def _list_untracked(cwd):
|
||||
mtime is captured so an in-place edit during the turn is still reviewed.
|
||||
|
||||
Uses ls-files (not status) for the UPS path: the index diff isn't needed,
|
||||
and ls-files --others only walks the worktree against .gitignore."""
|
||||
and ls-files --others only walks the worktree against .gitignore.
|
||||
|
||||
Decodes stdout/stderr as UTF-8 with errors="replace" instead of using
|
||||
text=True. With core.quotePath=false git emits raw UTF-8 bytes for
|
||||
non-ASCII filenames; text=True decodes via locale.getpreferredencoding()
|
||||
in strict mode — on Windows that's cp1252 with several undefined bytes
|
||||
(0x81/0x8D/0x8F/0x90/0x9D), all of which appear in UTF-8 encodings of
|
||||
common accented capitals (Á Í Ï Ð Ý) and most CJK/emoji codepoints.
|
||||
A non-ASCII filename in the worktree crashed the subprocess reader
|
||||
thread, left r.stdout=None, and propagated AttributeError out of the
|
||||
helper — silently losing the baseline snapshot every UserPromptSubmit.
|
||||
See anthropics/claude-plugins-official#2056. The sibling helpers in
|
||||
gitutil.py already follow the lenient pattern; this function and
|
||||
capture_git_baseline / _git_name_only / _git_status_porcelain were
|
||||
the holdouts."""
|
||||
try:
|
||||
repo = _git_toplevel(cwd) or cwd
|
||||
r = subprocess.run(
|
||||
[*GIT_CMD, "-c", "core.quotePath=false", "ls-files",
|
||||
"--others", "--exclude-standard", "-z"],
|
||||
cwd=repo, capture_output=True, text=True, timeout=15,
|
||||
cwd=repo, capture_output=True, timeout=15,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
debug_log(f"_list_untracked rc={r.returncode}: {r.stderr[:200]}")
|
||||
stderr_str = (r.stderr or b"").decode("utf-8", errors="replace")
|
||||
debug_log(f"_list_untracked rc={r.returncode}: {stderr_str[:200]}")
|
||||
return {}
|
||||
stdout = (r.stdout or b"").decode("utf-8", errors="replace")
|
||||
out = {}
|
||||
for p in r.stdout.split("\0"):
|
||||
for p in stdout.split("\0"):
|
||||
if not p:
|
||||
continue
|
||||
try:
|
||||
@@ -346,7 +377,9 @@ def _list_untracked(cwd):
|
||||
debug_log(f"_list_untracked: capped at {UNTRACKED_BASELINE_CAP}")
|
||||
break
|
||||
return out
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError, OSError, ValueError) as e:
|
||||
# ValueError guards against any future strict-decode regression
|
||||
# so the helper degrades to {} instead of crashing the hook.
|
||||
debug_log(f"_list_untracked error: {e}")
|
||||
return {}
|
||||
|
||||
|
||||
@@ -23,6 +23,12 @@ import sys
|
||||
import time
|
||||
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.
|
||||
NOOP_SYSTEM = 0 # claude_agent_sdk already importable in system python
|
||||
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():
|
||||
return NOOP_SYSTEM, "", ""
|
||||
|
||||
state_dir = Path(
|
||||
os.environ.get("SECURITY_WARNINGS_STATE_DIR")
|
||||
or os.path.expanduser("~/.claude/security")
|
||||
)
|
||||
state_dir = Path(_resolve_state_dir())
|
||||
venv = state_dir / "agent-sdk-venv"
|
||||
# Windows venvs put the interpreter at Scripts\python.exe; POSIX uses bin/python.
|
||||
if sys.platform == "win32":
|
||||
@@ -239,10 +242,7 @@ def _maybe_emit_user_notice(outcome: int, pv: int) -> str | None:
|
||||
if outcome != HOOK_PY_INCOMPATIBLE:
|
||||
return None
|
||||
try:
|
||||
state_dir = Path(
|
||||
os.environ.get("SECURITY_WARNINGS_STATE_DIR")
|
||||
or os.path.expanduser("~/.claude/security")
|
||||
)
|
||||
state_dir = Path(_resolve_state_dir())
|
||||
marker = state_dir / f".agentic_unavailable_notice_v{pv or 0}"
|
||||
if marker.exists():
|
||||
return None
|
||||
|
||||
@@ -259,19 +259,29 @@ def _git_reflog_recent_commits(repo_root, max_age_s=120, max_n=5):
|
||||
# %gs (the reflog subject) is `commit: <commit-msg first line>` and can
|
||||
# contain `|`; put it LAST so split("|", 2) leaves it intact. %H is
|
||||
# hex and %ct is integer, so the first two fields are delimiter-safe.
|
||||
#
|
||||
# Bytes + decode utf-8/replace: %gs embeds commit-message subjects
|
||||
# which git stores as raw bytes — commits can be authored in
|
||||
# latin-1 / cp1252 / shift-jis etc., and text=True would raise
|
||||
# UnicodeDecodeError in the subprocess reader thread on Windows
|
||||
# cp1252 (subprocess.run returns r.stdout=None, then
|
||||
# r.stdout.splitlines() AttributeErrors). Mirrors the existing
|
||||
# migration at security_reminder_hook.py:540 — same pattern was
|
||||
# missed here. See anthropics/claude-plugins-official#2056.
|
||||
r = subprocess.run(
|
||||
[*GIT_CMD, "log", "-g", "-n", str(max_n),
|
||||
"--format=%H|%ct|%gs", "HEAD"],
|
||||
cwd=repo_root, capture_output=True, text=True, timeout=5,
|
||||
cwd=repo_root, capture_output=True, timeout=5,
|
||||
)
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError, OSError, ValueError):
|
||||
return [], 0
|
||||
if r.returncode != 0:
|
||||
return [], 0
|
||||
stdout = (r.stdout or b"").decode("utf-8", errors="replace")
|
||||
import time as _time
|
||||
now = int(_time.time())
|
||||
fresh, stale = [], 0
|
||||
for idx, line in enumerate(r.stdout.splitlines()):
|
||||
for idx, line in enumerate(stdout.splitlines()):
|
||||
parts = line.split("|", 2)
|
||||
if len(parts) != 3:
|
||||
continue
|
||||
@@ -306,23 +316,31 @@ def _git_name_only(cwd, base, include_untracked=False):
|
||||
must distinguish None (error → don't trust as a filter) from set()
|
||||
(genuinely nothing changed). `-c core.quotePath=false -z` keeps non-ASCII
|
||||
and space-containing paths intact."""
|
||||
# Decode stdout/stderr as UTF-8 with errors="replace" instead of using
|
||||
# text=True. core.quotePath=false makes git emit raw UTF-8 for non-ASCII
|
||||
# paths, and text=True on Windows decodes via cp1252 strict — a non-ASCII
|
||||
# changed path would crash the subprocess reader thread, leave
|
||||
# result.stdout=None, and propagate AttributeError out of the helper.
|
||||
# Same fix shape as diffstate._list_untracked. See #2056.
|
||||
def _run(env):
|
||||
result = subprocess.run(
|
||||
[*GIT_CMD, "-c", "core.quotePath=false", "diff", "--name-only", "-z", base],
|
||||
cwd=cwd, capture_output=True, text=True, timeout=30,
|
||||
cwd=cwd, capture_output=True, timeout=30,
|
||||
env=env,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
debug_log(f"_git_name_only({base!r}) rc={result.returncode}: {result.stderr[:200]}")
|
||||
stderr_str = (result.stderr or b"").decode("utf-8", errors="replace")
|
||||
debug_log(f"_git_name_only({base!r}) rc={result.returncode}: {stderr_str[:200]}")
|
||||
return None
|
||||
return {p for p in result.stdout.split("\0") if p}
|
||||
stdout = (result.stdout or b"").decode("utf-8", errors="replace")
|
||||
return {p for p in stdout.split("\0") if p}
|
||||
|
||||
try:
|
||||
if not include_untracked:
|
||||
return _run(None)
|
||||
with _temp_index(cwd) as env:
|
||||
return _run(env)
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError, OSError, ValueError) as e:
|
||||
debug_log(f"_git_name_only({base!r}) error: {e}")
|
||||
return None
|
||||
|
||||
@@ -339,17 +357,22 @@ def _git_status_porcelain(cwd):
|
||||
collapses to `dir/`). Required so the untracked set subtracts cleanly
|
||||
against the UPS-time `_list_untracked` snapshot, which uses ls-files and
|
||||
therefore always lists individual files."""
|
||||
# Lenient decode: same UTF-8 + errors="replace" pattern as the
|
||||
# sibling helpers — a non-ASCII path in the worktree would otherwise
|
||||
# crash the cp1252 reader thread on Windows. See #2056.
|
||||
try:
|
||||
r = subprocess.run(
|
||||
[*GIT_CMD, "-c", "core.quotePath=false", "status",
|
||||
"--porcelain=v1", "-uall", "-z"],
|
||||
cwd=cwd, capture_output=True, text=True, timeout=30,
|
||||
cwd=cwd, capture_output=True, timeout=30,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
debug_log(f"_git_status_porcelain rc={r.returncode}: {r.stderr[:200]}")
|
||||
stderr_str = (r.stderr or b"").decode("utf-8", errors="replace")
|
||||
debug_log(f"_git_status_porcelain rc={r.returncode}: {stderr_str[:200]}")
|
||||
return None, None
|
||||
tracked, untracked = set(), set()
|
||||
entries = r.stdout.split("\0")
|
||||
stdout = (r.stdout or b"").decode("utf-8", errors="replace")
|
||||
entries = stdout.split("\0")
|
||||
i = 0
|
||||
while i < len(entries):
|
||||
e = entries[i]
|
||||
@@ -368,7 +391,9 @@ def _git_status_porcelain(cwd):
|
||||
i += 1
|
||||
i += 1
|
||||
return tracked, untracked
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError, OSError, ValueError) as e:
|
||||
# ValueError guards against any future strict-decode regression
|
||||
# so the helper degrades to (None, None) instead of crashing.
|
||||
debug_log(f"_git_status_porcelain error: {e}")
|
||||
return None, None
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ from typing import Optional, Tuple, Dict, Any, List
|
||||
|
||||
import extensibility
|
||||
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
|
||||
|
||||
|
||||
@@ -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
|
||||
# agentic_review() — duplicated here so the 3P path doesn't require
|
||||
# the agentic path to have run first.
|
||||
_state_dir = os.environ.get(
|
||||
"SECURITY_WARNINGS_STATE_DIR",
|
||||
os.path.expanduser("~/.claude/security"),
|
||||
)
|
||||
_state_dir = _resolve_state_dir()
|
||||
_inject_agent_sdk_venv_into_syspath(_state_dir)
|
||||
try:
|
||||
import asyncio as _asyncio # noqa: F811
|
||||
@@ -1145,10 +1142,7 @@ def agentic_review(
|
||||
# ~/.claude/security/ with the SDK installed; try that as a fallback
|
||||
# before giving up. The system import is attempted first so users
|
||||
# who DO have it never touch the venv.
|
||||
_state_dir = os.environ.get(
|
||||
"SECURITY_WARNINGS_STATE_DIR",
|
||||
os.path.expanduser("~/.claude/security"),
|
||||
)
|
||||
_state_dir = _resolve_state_dir()
|
||||
_venv_tried = _inject_agent_sdk_venv_into_syspath(_state_dir)
|
||||
try:
|
||||
import asyncio as _asyncio # noqa: F811
|
||||
|
||||
@@ -94,6 +94,9 @@ Only use exec() if you absolutely need shell features and the input is guarantee
|
||||
},
|
||||
{
|
||||
"ruleName": "new_function_injection",
|
||||
# JS-only construct: gate to JS/TS files so docs/.md and other prose
|
||||
# mentioning "new Function" don't trip the warning.
|
||||
"path_filter": lambda p: p.endswith(_JS_EXTS),
|
||||
"substrings": ["new Function"],
|
||||
"reminder": "\u26a0\ufe0f Security Warning: Using new Function() with string interpolation is a CODE INJECTION vulnerability. If any variable is concatenated or interpolated into the function body string, an attacker controlling that variable can execute arbitrary code. Use safe alternatives: for property access use obj[key] or array.reduce((o, k) => o[k], root); for computation use a safe expression parser. NEVER interpolate untrusted strings into new Function() bodies.",
|
||||
},
|
||||
@@ -107,16 +110,24 @@ Only use exec() if you absolutely need shell features and the input is guarantee
|
||||
},
|
||||
{
|
||||
"ruleName": "react_dangerously_set_html",
|
||||
# JS/TS-only (React); gate so .md docs / .py / .go files don't trip.
|
||||
"path_filter": lambda p: p.endswith(_JS_EXTS),
|
||||
"substrings": ["dangerouslySetInnerHTML"],
|
||||
"reminder": "⚠️ Security Warning: dangerouslySetInnerHTML can lead to XSS vulnerabilities if used with untrusted content. Ensure all content is properly sanitized using an HTML sanitizer library like DOMPurify, or use safe alternatives.",
|
||||
},
|
||||
{
|
||||
"ruleName": "document_write_xss",
|
||||
# Browser DOM API: only meaningful in JS/TS source.
|
||||
"path_filter": lambda p: p.endswith(_JS_EXTS),
|
||||
"substrings": ["document.write"],
|
||||
"reminder": "⚠️ Security Warning: document.write() can be exploited for XSS attacks and has performance issues. Use DOM manipulation methods like createElement() and appendChild() instead.",
|
||||
},
|
||||
{
|
||||
"ruleName": "innerHTML_xss",
|
||||
# Browser DOM API: only meaningful in JS/TS source. Closes FPs like
|
||||
# docs/example HTML, playground/self-contained skills that hardcode
|
||||
# innerHTML strings with zero user input (#410).
|
||||
"path_filter": lambda p: p.endswith(_JS_EXTS),
|
||||
"substrings": [".innerHTML =", ".innerHTML="],
|
||||
"reminder": "⚠️ Security Warning: Setting innerHTML with untrusted content can lead to XSS vulnerabilities. Use textContent for plain text or safe DOM methods for HTML content. If you need HTML support, consider using an HTML sanitizer library such as DOMPurify.",
|
||||
},
|
||||
@@ -217,11 +228,15 @@ Additionally, validate user inputs:
|
||||
},
|
||||
{
|
||||
"ruleName": "outerHTML_xss",
|
||||
# Browser DOM API: only meaningful in JS/TS source.
|
||||
"path_filter": lambda p: p.endswith(_JS_EXTS),
|
||||
"substrings": [".outerHTML =", ".outerHTML="],
|
||||
"reminder": "⚠️ Security Warning: Use textContent or sanitize with DOMPurify. outerHTML assignment is an XSS sink equivalent to innerHTML.",
|
||||
},
|
||||
{
|
||||
"ruleName": "insertAdjacentHTML_xss",
|
||||
# Browser DOM API: only meaningful in JS/TS source.
|
||||
"path_filter": lambda p: p.endswith(_JS_EXTS),
|
||||
"substrings": [".insertAdjacentHTML("],
|
||||
"reminder": "⚠️ Security Warning: Use insertAdjacentText() or sanitize with DOMPurify. insertAdjacentHTML is an XSS sink.",
|
||||
},
|
||||
|
||||
@@ -82,6 +82,7 @@ from _base import ( # noqa: E402,F401
|
||||
PROVENANCE_TAG, PROVENANCE_BANNER,
|
||||
_read_plugin_version_int, _PV, _USAGE, _USAGE_LOCK,
|
||||
_PRICE_PER_MTOK, _PRICE_DEFAULT, _record_usage, _usage_metrics,
|
||||
state_dir as _resolve_state_dir,
|
||||
)
|
||||
import extensibility # noqa: E402
|
||||
from patterns import ( # noqa: E402,F401
|
||||
@@ -1971,10 +1972,7 @@ def handle_stop_hook(input_data):
|
||||
})
|
||||
sys.exit(0)
|
||||
|
||||
_SDK_BOOTSTRAP_THROTTLE = os.path.join(
|
||||
os.environ.get("SECURITY_WARNINGS_STATE_DIR")
|
||||
or os.path.expanduser("~/.claude/security"),
|
||||
".sdk_bootstrap_spawned")
|
||||
_SDK_BOOTSTRAP_THROTTLE = os.path.join(_resolve_state_dir(), ".sdk_bootstrap_spawned")
|
||||
|
||||
def _maybe_bootstrap_agent_sdk_async():
|
||||
"""Fire-and-forget SDK bootstrap, for remote-pod environments.
|
||||
|
||||
@@ -19,7 +19,7 @@ import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
from _base import debug_log
|
||||
from _base import debug_log, state_dir as _state_dir
|
||||
|
||||
|
||||
def _state_key(session_id):
|
||||
@@ -36,20 +36,20 @@ def _state_key(session_id):
|
||||
|
||||
def get_state_file(session_id):
|
||||
"""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")
|
||||
|
||||
|
||||
def get_lock_file(session_id):
|
||||
"""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")
|
||||
|
||||
|
||||
def cleanup_old_state_files():
|
||||
"""Remove state files and lock files older than 30 days."""
|
||||
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):
|
||||
return
|
||||
|
||||
|
||||
Reference in New Issue
Block a user