Detect a Symlink Swap on a Watched Compliance File
A classic TOCTOU-style attack: a path that's expected to hold a plain file gets swapped for a symlink, so whatever reads it next actually reads something else entirely, outside the repo. This checks a newly-created file at a watched path and flags it if it's a symlink resolving outside the project root.
1. Save this as the hook script
#!/usr/bin/env python3
"""FileChanged hook — flags a symlink created at a watched path that
resolves outside the project root. Wire with matcher "created"."""
import json, sys, os
from pathlib import Path
data = json.load(sys.stdin)
path = Path(data.get("file_path", ""))
cwd = Path(data.get("cwd", "."))
if data.get("change_type") != "created" or not path.is_symlink():
sys.exit(0)
target = path.resolve()
if not str(target).startswith(str(cwd.resolve())):
print(f"ALERT: {path} is a new symlink resolving to {target}, outside "
f"the project root — possible symlink-swap on a watched path.", file=sys.stderr)
sys.exit(1)
sys.exit(0)
Save to a path inside your project, e.g. .claude/hooks/detect-symlink-swap-watched-file.sh, then chmod +x it.
2. Wire it into .claude/settings.json
{
"hooks": {
"FileChanged": [
{
"matcher": "created",
"hooks": [
{"type": "command", "command": "python3 .claude/hooks/detect-symlink-swap-watched-file.py"}
]
}
]
}
}
Before you use this
A hook runs with your local shell's permissions — review the script above before adding it, the same way you'd review any script you didn't write yourself. See our guide on hook security risks for the specific CVEs this category of feature has already produced.
Related hooks