You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
While evaluating the lifecycle hooks in hooks/, a multi-pass review (correctness, security, and test-coverage) surfaced several security and data-integrity issues in the opt-in hooks — sdd-cache-pre.sh, sdd-cache-post.sh, and simplify-ignore.sh.
These hooks are not wired by default (hooks/hooks.json only registers session-start.sh, which is fine), so this affects users who manually enable the WebFetch cache or the simplify-ignore feature per SDD-CACHE.md / SIMPLIFY-IGNORE.md. Filing publicly since there's no SECURITY.md / private channel. Happy to convert to a private report if you'd prefer.
session-start.sh reviewed clean (trusted local input, correct jq --arg escaping).
Findings
#
Severity
File
Class
Issue
1
Critical
sdd-cache-pre.sh:71-74, sdd-cache-post.sh:82
SSRF
curl -L "$URL" with no scheme/host allowlist, following redirects, fires automatically on every WebFetch
No TTL cap; any 304 serves cached body forever as authoritative "unchanged" content
3
High
sdd-cache-post.sh:75-77, sdd-cache-pre.sh:50-51
Tamperable cache
Predictable filenames (sha256(URL)), default perms, in-tree .claude/sdd-cache/; a committed/crafted cache entry is trusted on next 304
4
High
simplify-ignore.sh:153-154,202,209,287,291
Symlink overwrite / TOCTOU
Backup/restore writes via cat > "$target" on an untrusted file_path with no symlink check; arbitrary-file-overwrite primitive
5
Critical (integrity)
simplify-ignore.sh filter/expand path
Data loss
Non-atomic in-place rewrite under set -e; an abort (e.g. missing perl, full disk) can leave the real file holding placeholders after the backup was overwritten
1. SSRF via unvalidated curl (Critical)
URL comes verbatim from .tool_input.url and is passed to curl ... -L with no scheme/host validation and redirect-following enabled. A WebFetch to an attacker-influenced link (e.g. via prompt injection in a fetched page) can 302-redirect the hook's automatic out-of-band request to internal targets — cloud metadata (169.254.169.254), localhost admin ports, etc.
Highest-value single fix: drop -L (or re-validate the host on every hop), require https://, and reject internal/metadata hosts before any curl.
2. Durable cache poisoning (High)
fetched_at is stored but never enforced as a max age. Once an entry is cached, any subsequent 304 Not Modified causes the stored body to be served to the agent verbatim and labeled "Revalidated … unchanged" — with no TTL. A malicious or compromised origin (or MITM on a non-HTTPS URL via #1) can pin injected content that persists across sessions.
Fix: enforce a TTL cap before serving from cache regardless of 304; optionally store + verify a content hash.
3. Tamperable / disclosing cache (High)
Cache files are written to ${CLAUDE_PROJECT_DIR:-$PWD}/.claude/sdd-cache/<sha>.json with default umask and no integrity protection; the filename is sha256(URL), fully predictable. A crafted entry (even one committed to a repo) is trusted and served on the next matching WebFetch + 304. Fetched bodies (possibly authenticated/internal content) are persisted in plaintext inside the working tree.
Fix:chmod 700 the cache dir / umask 077; add .claude/sdd-cache/ to .gitignore guidance; don't trust pre-existing entries without an integrity check.
4. Symlink/arbitrary-file overwrite (High)
file_path (untrusted) drives backup-and-rewrite on Read and write-back on Edit/Write/Stop via cat > "$orig", which follows symlinks. There's no [ -L ] check and the path→file mapping is a 16-char hash with no inode re-verification, opening a session-long TOCTOU window. This is an arbitrary-overwrite primitive against any user-writable file (~/.bashrc, etc.).
Fix:[ -L "$FILE_PATH" ] && exit 0 at entry; re-verify the target is a regular non-symlink (by inode) before every write-back; write via temp + mv -T.
5. Non-atomic rewrite under set -e (Critical integrity / data loss)
simplify-ignore.sh rewrites the user's real file in place across multiple hook events. With set -euo pipefail, an unguarded failure inside filter_file (e.g. perl absent on a minimal Git Bash — line 137/269 hard-depend on perl though only jq/shasum are declared; or a full/read-only cache dir) can abort after : > "$dest" and after the backup was overwritten, leaving the on-disk file full of BLOCK_<hash> placeholders and no clean backup.
Fix: declare perl as a dependency and check up front (or replace with pure-shell newline handling); trap-guard the rewrite; make the final swap atomic (mv temp into place).
Additional notes
simplify-ignore.sh file identity is sha(path)|cut -c1-16 with no path normalization — the same file as C:\… vs /c/… can be filtered under two IDs, and Stop can restore stale content over edits. Normalize the path (e.g. cygpath/realpath) before hashing.
BLOCK_<hash> placeholder expansion uses global substring replace with fuzzy/last-resort fallbacks; protected content that itself contains a BLOCK_<hash>-shaped string can be corrupted on expand. Consider a sentinel that cannot occur in source.
The two sdd-cache scripts have no tests; the data-loss-critical expand/restore half of simplify-ignore.sh is also untested. Highest-value missing test: round-trip fidelity (filter → expand → original bytes recovered).
Suggested minimum changes to make the opt-in hooks safe to enable
Summary
While evaluating the lifecycle hooks in
hooks/, a multi-pass review (correctness, security, and test-coverage) surfaced several security and data-integrity issues in the opt-in hooks —sdd-cache-pre.sh,sdd-cache-post.sh, andsimplify-ignore.sh.These hooks are not wired by default (
hooks/hooks.jsononly registerssession-start.sh, which is fine), so this affects users who manually enable the WebFetch cache or the simplify-ignore feature perSDD-CACHE.md/SIMPLIFY-IGNORE.md. Filing publicly since there's noSECURITY.md/ private channel. Happy to convert to a private report if you'd prefer.session-start.shreviewed clean (trusted local input, correctjq --argescaping).Findings
sdd-cache-pre.sh:71-74,sdd-cache-post.sh:82curl -L "$URL"with no scheme/host allowlist, following redirects, fires automatically on every WebFetchsdd-cache-post.sh:105-128,sdd-cache-pre.sh:62-106304serves cached body forever as authoritative "unchanged" contentsdd-cache-post.sh:75-77,sdd-cache-pre.sh:50-51sha256(URL)), default perms, in-tree.claude/sdd-cache/; a committed/crafted cache entry is trusted on next 304simplify-ignore.sh:153-154,202,209,287,291cat > "$target"on an untrustedfile_pathwith no symlink check; arbitrary-file-overwrite primitivesimplify-ignore.shfilter/expand pathset -e; an abort (e.g. missingperl, full disk) can leave the real file holding placeholders after the backup was overwritten1. SSRF via unvalidated
curl(Critical)URLcomes verbatim from.tool_input.urland is passed tocurl ... -Lwith no scheme/host validation and redirect-following enabled. A WebFetch to an attacker-influenced link (e.g. via prompt injection in a fetched page) can 302-redirect the hook's automatic out-of-band request to internal targets — cloud metadata (169.254.169.254),localhostadmin ports, etc.Highest-value single fix: drop
-L(or re-validate the host on every hop), requirehttps://, and reject internal/metadata hosts before anycurl.2. Durable cache poisoning (High)
fetched_atis stored but never enforced as a max age. Once an entry is cached, any subsequent304 Not Modifiedcauses the stored body to be served to the agent verbatim and labeled "Revalidated … unchanged" — with no TTL. A malicious or compromised origin (or MITM on a non-HTTPS URL via #1) can pin injected content that persists across sessions.Fix: enforce a TTL cap before serving from cache regardless of 304; optionally store + verify a content hash.
3. Tamperable / disclosing cache (High)
Cache files are written to
${CLAUDE_PROJECT_DIR:-$PWD}/.claude/sdd-cache/<sha>.jsonwith default umask and no integrity protection; the filename issha256(URL), fully predictable. A crafted entry (even one committed to a repo) is trusted and served on the next matching WebFetch + 304. Fetched bodies (possibly authenticated/internal content) are persisted in plaintext inside the working tree.Fix:
chmod 700the cache dir /umask 077; add.claude/sdd-cache/to.gitignoreguidance; don't trust pre-existing entries without an integrity check.4. Symlink/arbitrary-file overwrite (High)
file_path(untrusted) drives backup-and-rewrite on Read and write-back on Edit/Write/Stop viacat > "$orig", which follows symlinks. There's no[ -L ]check and the path→file mapping is a 16-char hash with no inode re-verification, opening a session-long TOCTOU window. This is an arbitrary-overwrite primitive against any user-writable file (~/.bashrc, etc.).Fix:
[ -L "$FILE_PATH" ] && exit 0at entry; re-verify the target is a regular non-symlink (by inode) before every write-back; write via temp +mv -T.5. Non-atomic rewrite under
set -e(Critical integrity / data loss)simplify-ignore.shrewrites the user's real file in place across multiple hook events. Withset -euo pipefail, an unguarded failure insidefilter_file(e.g.perlabsent on a minimal Git Bash — line 137/269 hard-depend onperlthough onlyjq/shasumare declared; or a full/read-only cache dir) can abort after: > "$dest"and after the backup was overwritten, leaving the on-disk file full ofBLOCK_<hash>placeholders and no clean backup.Fix: declare
perlas a dependency and check up front (or replace with pure-shell newline handling);trap-guard the rewrite; make the final swap atomic (mvtemp into place).Additional notes
simplify-ignore.shfile identity issha(path)|cut -c1-16with no path normalization — the same file asC:\…vs/c/…can be filtered under two IDs, and Stop can restore stale content over edits. Normalize the path (e.g.cygpath/realpath) before hashing.BLOCK_<hash>placeholder expansion uses global substring replace with fuzzy/last-resort fallbacks; protected content that itself contains aBLOCK_<hash>-shaped string can be corrupted on expand. Consider a sentinel that cannot occur in source.sdd-cachescripts have no tests; the data-loss-critical expand/restore half ofsimplify-ignore.shis also untested. Highest-value missing test: round-trip fidelity (filter → expand → original bytes recovered).Suggested minimum changes to make the opt-in hooks safe to enable
sdd-cache-*: requirehttps://, drop-L(or re-validate host per hop), block internal/metadata hosts. (fixes Add Claude Code plugin support and fix stale swe-skills references #1)sdd-cache-*: enforce a TTL cap before serving cache;chmod 700the cache dir; don't trust pre-existing entries. (fixes Implement /code-simplify skill #2, Fixes #2 - adds code simplifier v0.1 #3)simplify-ignore.sh: reject symlinks and re-verify a regular-file target before write-back; atomic,trap-guarded rewrite; declare/checkperl. (fixes docs: add code-simplification to README #4, [Bug]: browser-testing-with-devtools - Gen Agent Trust failed & Snyk warnings #5)Methodology: findings independently corroborated across three review passes. Glad to follow up with PRs for any of the above.