diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index e0259a4..79f7162 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -39,11 +39,18 @@ Skills follow the [Claude Code skills documentation](https://docs.anthropic.com/ **Guards (PreToolUse):** -- `validate-bash-commands.py` — Validates Bash tool invocations +- `validate-bash-commands.py` — Suggests dedicated tools when Bash invokes grep, find, cat, sed, or awk +- `config-protection.sh` — Warns when editing linter/formatter config files (eslint, biome, prettier, tsconfig, etc.) +- `prompt-injection-guard.py` — Detects prompt injection patterns in content being written to files **Formatters (PostToolUse):** -- `auto-format.sh` — Runs prettier on Edit/Write automatically +- `auto-format.sh` — Runs prettier/gofmt on Edit/Write automatically + +**Monitors (PostToolUse):** + +- `suggest-compact.sh` — Nudges compaction every ~30 tool calls +- `context-monitor.sh` — Tracks context window usage and emits warnings at thresholds **Session lifecycle:** diff --git a/hooks/auto-format.sh b/hooks/auto-format.sh index 39d3097..b7c75be 100755 --- a/hooks/auto-format.sh +++ b/hooks/auto-format.sh @@ -9,46 +9,46 @@ file_path="${TOOL_FILE_PATH:-}" if [ -z "$file_path" ]; then - # Fallback: read from stdin JSON (PostToolUse payload structure) - if command -v jq &>/dev/null; then - if ! file_path=$(jq -r '.tool_input.file_path // empty' 2>/dev/null); then - exit 0 # Graceful exit if JSON parsing fails - fi - else - exit 0 # No jq available, can't parse stdin - fi + # Fallback: read from stdin JSON (PostToolUse payload structure) + if command -v jq &>/dev/null; then + if ! file_path=$(jq -r '.tool_input.file_path // empty' 2>/dev/null); then + exit 0 # Graceful exit if JSON parsing fails + fi + else + exit 0 # No jq available, can't parse stdin + fi fi if [ -z "$file_path" ]; then - exit 0 + exit 0 fi # Run a formatter if available, log failures try_format() { - local tool="$1" file="$2" - shift 2 - if command -v "$tool" &>/dev/null; then - if ! "$tool" "$@" "$file" 2>/dev/null; then - echo "[auto-format] $tool failed on $file" >&2 - fi - fi + local tool="$1" file="$2" + shift 2 + if command -v "$tool" &>/dev/null; then + if ! "$tool" "$@" "$file" 2>/dev/null; then + echo "[auto-format] $tool failed on $file" >&2 + fi + fi } # Format based on file extension case "$file_path" in - *.go) - try_format gofmt "$file_path" -w - ;; - *.ts | *.tsx | *.js | *.jsx | *.json | *.yaml | *.yml) - try_format prettier "$file_path" --write - ;; - *.md) - basename=$(basename "$file_path") - if [ "$basename" = "walkthrough.md" ]; then - exit 0 # Managed by showboat — prettier would break verified output blocks - fi - try_format prettier "$file_path" --write - ;; +*.go) + try_format gofmt "$file_path" -w + ;; +*.ts | *.tsx | *.js | *.jsx | *.json | *.yaml | *.yml) + try_format bunx "$file_path" prettier --write + ;; +*.md) + basename=$(basename "$file_path") + if [ "$basename" = "walkthrough.md" ]; then + exit 0 # Managed by showboat — prettier would break verified output blocks + fi + try_format bunx "$file_path" prettier --write + ;; esac exit 0 diff --git a/hooks/config-protection.sh b/hooks/config-protection.sh index 5ed6648..f46db1e 100755 --- a/hooks/config-protection.sh +++ b/hooks/config-protection.sh @@ -7,38 +7,39 @@ file_path="${TOOL_FILE_PATH:-}" if [ -z "$file_path" ]; then - if command -v jq &>/dev/null; then - if ! file_path=$(jq -r '.tool_input.file_path // empty' 2>/dev/null); then - exit 0 - fi - else - exit 0 - fi + if command -v jq &>/dev/null; then + if ! file_path=$(jq -r '.tool_input.file_path // empty' 2>/dev/null); then + exit 0 + fi + else + exit 0 + fi fi if [ -z "$file_path" ]; then - exit 0 + exit 0 fi basename=$(basename "$file_path") # Protected config file patterns case "$basename" in - .eslintrc* | eslint.config.* | \ - biome.json | \ - .prettierrc* | prettier.config.* | \ - .golangci.yml | .golangci.yaml | \ - ruff.toml | \ - .shellcheckrc | \ - .markdownlint* | \ - .yamllint | .yamllint.yaml | \ - .editorconfig | \ - .stylelintrc*) - echo "Warning: modifying linter/formatter config ($basename). Fix the code, not the config." >&2 - ;; - pyproject.toml) - echo "Warning: modifying pyproject.toml — if changing ruff/linter settings, fix the code instead." >&2 - ;; +.eslintrc* | eslint.config.* | \ + biome.json | \ + .prettierrc* | prettier.config.* | \ + .golangci.yml | .golangci.yaml | \ + ruff.toml | \ + .shellcheckrc | \ + .markdownlint* | \ + .yamllint | .yamllint.yaml | \ + .editorconfig | \ + .stylelintrc* | \ + tsconfig.json | tsconfig.*.json) + echo "Warning: modifying linter/formatter config ($basename). Fix the code, not the config." >&2 + ;; +pyproject.toml) + echo "Warning: modifying pyproject.toml — if changing ruff/linter settings, fix the code instead." >&2 + ;; esac exit 0 diff --git a/hooks/suggest-compact.sh b/hooks/suggest-compact.sh index 9949354..6260739 100755 --- a/hooks/suggest-compact.sh +++ b/hooks/suggest-compact.sh @@ -4,26 +4,23 @@ # # Note: Intentionally no 'set -euo pipefail' - hooks must always exit 0 -counter_file="/tmp/claude-compact-count-$$" +# Use session ID from stdin JSON if available, fall back to PID +session_id="" +if raw=$(cat 2>/dev/null); then + if command -v jq &>/dev/null && [ -n "$raw" ]; then + session_id=$(echo "$raw" | jq -r '.session_id // empty' 2>/dev/null) || true + fi +fi +session_id="${session_id:-$$}" +counter_file="/tmp/claude-compact-count-${session_id}" # SessionStart resets the counter (hook_event env var from Claude Code) if [ "${HOOK_EVENT:-}" = "SessionStart" ]; then - # Clean up any stale counter files from previous sessions - rm -f /tmp/claude-compact-count-* 2>/dev/null - rm -f /tmp/claude-ctx-*.json /tmp/claude-ctx-warn-* 2>/dev/null - echo "0" >"$counter_file" - exit 0 -fi - -# Find existing counter file for this session or any recent one -if [ ! -f "$counter_file" ]; then - # Try to find an existing counter file - existing=$(find /tmp -maxdepth 1 -name 'claude-compact-count-*' -print -quit 2>/dev/null) - if [ -n "$existing" ]; then - counter_file="$existing" - else - echo "0" >"$counter_file" - fi + # Clean up any stale counter files from previous sessions + rm -f /tmp/claude-compact-count-* 2>/dev/null + rm -f /tmp/claude-ctx-*.json /tmp/claude-ctx-warn-* 2>/dev/null + echo "0" >"$counter_file" + exit 0 fi # Increment counter @@ -33,7 +30,7 @@ echo "$count" >"$counter_file" # Suggest compaction every 30 tool calls if [ $((count % 30)) -eq 0 ]; then - echo "You've made $count tool calls this session. Consider running /compact at a natural breakpoint." >&2 + echo "You've made $count tool calls this session. Consider running /compact at a natural breakpoint." >&2 fi exit 0 diff --git a/hooks/validate-bash-commands.py b/hooks/validate-bash-commands.py index 50af2c1..afa3325 100755 --- a/hooks/validate-bash-commands.py +++ b/hooks/validate-bash-commands.py @@ -31,10 +31,16 @@ warnings.append(message) if warnings: - print("⚠️ Command suggestions:") - for warning in warnings: - print(f" • {warning}") - # Don't block, just inform (output to stdout so Claude sees it) + output = { + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "additionalContext": ( + "Command suggestions: " + + "; ".join(warnings) + ), + } + } + json.dump(output, sys.stdout) sys.exit(0) diff --git a/settings.json b/settings.json index d8edbaf..6ffd275 100644 --- a/settings.json +++ b/settings.json @@ -21,6 +21,7 @@ "Write(*.tsx)", "Write(*.yaml)", "Write(*.yml)", + "Write(*.sh)", "Bash(brew:*)", "Bash(bun:*)", "Bash(bunx:*)", diff --git a/skills/obsidian-cli/SKILL.md b/skills/obsidian-cli/SKILL.md index 39bf68f..7e435b8 100644 --- a/skills/obsidian-cli/SKILL.md +++ b/skills/obsidian-cli/SKILL.md @@ -1,5 +1,4 @@ --- -name: obsidian-cli description: Interacts with Obsidian vaults using the Obsidian CLI to read, create, and manage notes, tasks, properties, tags, and more. Also supports plugin and theme development with commands to reload plugins, run JavaScript, capture errors, take screenshots, and inspect the DOM. Use when the user asks to interact with their Obsidian vault, manage notes, add to daily note, find notes about a topic, check tasks, append to a note, query the vault, list tags, list files, manage bookmarks, check sync status, view file history, use templates, query bases, run QuickAdd, perform vault operations from the command line, or develop and debug Obsidian plugins and themes. allowed-tools: Bash ---