Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .claude/hooks/auto-lint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
set -euo pipefail

# PostToolUse hook: auto-lints files after Write/Edit.
# If lint fails, blocks Claude with errors so it self-corrects.
# If lint passes, exits silently (no context waste).

INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path')

# Skip if no file path or null
[ -z "$FILE_PATH" ] || [ "$FILE_PATH" = "null" ] && exit 0

# Skip files outside the project
[[ "$FILE_PATH" == "$CLAUDE_PROJECT_DIR/"* ]] || exit 0

# Skip non-existent files (e.g. after a failed write)
[ -f "$FILE_PATH" ] || exit 0

EXTENSION="${FILE_PATH##*.}"
LINT_OUTPUT=""
LINT_EXIT=0

case "$EXTENSION" in
ts|tsx|js|jsx|json|css)
LINT_OUTPUT=$(biome check --write "$FILE_PATH" 2>&1) || LINT_EXIT=$?
;;
py)
LINT_OUTPUT=$(
cd "$CLAUDE_PROJECT_DIR/packages/execution" \
&& uv run ruff check --fix "$FILE_PATH" 2>&1 \
&& uv run ruff format "$FILE_PATH" 2>&1
) || LINT_EXIT=$?
;;
*)
exit 0
;;
esac

if [ $LINT_EXIT -ne 0 ]; then
# Strip ANSI codes and send errors back to Claude
CLEAN_OUTPUT=$(echo "$LINT_OUTPUT" | sed 's/\x1b\[[0-9;]*m//g')
jq -n --arg reason "$CLEAN_OUTPUT" '{"decision": "block", "reason": $reason}'
exit 0
fi

# Lint passed — exit silently
exit 0
96 changes: 96 additions & 0 deletions .claude/hooks/command-interceptor.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#!/usr/bin/env bash
set -euo pipefail

# PreToolUse hook: intercepts test/lint/build Bash commands,
# wraps them in run_silent.sh, and injects fail-fast flags.
# Non-matching commands pass through untouched.

INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
TIMEOUT=$(echo "$INPUT" | jq -r '.tool_input.timeout // 120000')
DESC=$(echo "$INPUT" | jq -r '.tool_input.description // ""')
RUN_SILENT="$CLAUDE_PROJECT_DIR/scripts/run_silent.sh"

# --- Skip: compound commands and shell metacharacters ---
if [[ "$COMMAND" == *"&&"* ]] || [[ "$COMMAND" == *"||"* ]] || [[ "$COMMAND" == *";"* ]] \
|| [[ "$COMMAND" == *'$('* ]] || [[ "$COMMAND" == *'`'* ]] || [[ "$COMMAND" == *"|"* ]] \
|| [[ "$COMMAND" == *">("* ]] || [[ "$COMMAND" == *"<("* ]] \
|| [[ "$COMMAND" == *">"* ]] || [[ "$COMMAND" == *"<"* ]]; then
exit 0
fi

# --- Skip: interactive, dev, install, git, docker ---
case "$COMMAND" in
git\ *|docker\ *|docker-compose\ *) exit 0 ;;
pnpm\ dev*|pnpm\ run\ dev*|pnpm\ add*) exit 0 ;;
pnpm\ install*|pnpm\ i\ *|pnpm\ i) exit 0 ;;
uv\ add*|uv\ sync*|uv\ pip*) exit 0 ;;
cd\ *|ls*|cat\ *|echo\ *|which\ *|pwd*) exit 0 ;;
mkdir\ *|rm\ *|cp\ *|mv\ *|chmod\ *) exit 0 ;;
gh\ *|curl\ *|wget\ *) exit 0 ;;
esac

# Skip anything with --watch or --interactive
if [[ "$COMMAND" == *"--watch"* ]] || [[ "$COMMAND" == *"--interactive"* ]]; then
exit 0
fi

# --- Match: wrap in run_silent + inject fail-fast ---
WRAPPED=""

case "$COMMAND" in
# vitest with fail-fast (bare `vitest` excluded — it launches watch mode)
vitest\ run*)
if [[ "$COMMAND" != *"--bail"* ]]; then
WRAPPED="$RUN_SILENT $COMMAND --bail 1"
else
WRAPPED="$RUN_SILENT $COMMAND"
fi ;;

# pytest with fail-fast
pytest\ *|pytest|uv\ run\ pytest\ *|uv\ run\ pytest)
if [[ "$COMMAND" != *"-x"* ]]; then
WRAPPED="$RUN_SILENT $COMMAND -x"
else
WRAPPED="$RUN_SILENT $COMMAND"
fi ;;

# pnpm scripts
pnpm\ test*|pnpm\ run\ test*) WRAPPED="$RUN_SILENT $COMMAND" ;;
pnpm\ typecheck*|pnpm\ run\ typecheck*) WRAPPED="$RUN_SILENT $COMMAND" ;;
pnpm\ lint*|pnpm\ run\ lint*) WRAPPED="$RUN_SILENT $COMMAND" ;;
pnpm\ build*|pnpm\ run\ build*) WRAPPED="$RUN_SILENT $COMMAND" ;;
pnpm\ verify*|pnpm\ run\ verify*) WRAPPED="$RUN_SILENT $COMMAND" ;;
pnpm\ check*|pnpm\ run\ check*) WRAPPED="$RUN_SILENT $COMMAND" ;;
pnpm\ format*|pnpm\ run\ format*) WRAPPED="$RUN_SILENT $COMMAND" ;;

# Direct tool invocations
turbo\ run\ *) WRAPPED="$RUN_SILENT $COMMAND" ;;
tsc\ *) WRAPPED="$RUN_SILENT $COMMAND" ;;
biome\ check*) WRAPPED="$RUN_SILENT $COMMAND" ;;
ruff\ check*) WRAPPED="$RUN_SILENT $COMMAND" ;;
ruff\ format*) WRAPPED="$RUN_SILENT $COMMAND" ;;
esac

# No match — pass through silently
if [ -z "$WRAPPED" ]; then
exit 0
fi

# Emit rewrite JSON: wrap command + auto-approve
jq -n \
--arg cmd "$WRAPPED" \
--argjson timeout "$TIMEOUT" \
--arg desc "$DESC" \
'{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "Context backpressure: wrapping in run_silent",
"updatedInput": {
"command": $cmd,
"timeout": $timeout,
"description": $desc
}
}
}'
28 changes: 28 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/command-interceptor.sh",
"timeout": 5
}
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/auto-lint.sh",
"timeout": 30
}
]
}
]
}
}
16 changes: 16 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,22 @@ Biome handles formatting and linting. No ESLint.

---

## Context backpressure

Three hooks manage output automatically:

1. **PreToolUse** wraps test/lint/build commands in `scripts/run_silent.sh`:
success → `✓ command (Xs)`, failure → filtered output only.
2. **PostToolUse** auto-lints after every Write/Edit via biome (TS) or
ruff (Python). Lint failures block until fixed — do not suppress them.
3. **Fail-fast** flags (`pytest -x`, `vitest --bail 1`) are injected
automatically.

Do not pipe to `/dev/null` or truncate with `head`/`tail`. The hooks
handle it. For full debug output, prefix with `env VERBOSE=1`.

---

## Skills (load when relevant)

```
Expand Down
55 changes: 55 additions & 0 deletions scripts/run_silent.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
set -euo pipefail

# Context-efficient backpressure wrapper.
# Success: prints a single summary line with test counts (if applicable).
# Failure: prints filtered output (no ANSI, no noise).
# Bypass: VERBOSE=1 ./scripts/run_silent.sh <command>

if [ "${VERBOSE:-}" = "1" ]; then
exec "$@"
fi

LABEL="$*"
TMPFILE=$(mktemp)
trap 'rm -f "$TMPFILE"' EXIT INT TERM
SECONDS=0

set +e
"$@" > "$TMPFILE" 2>&1
EXIT_CODE=$?
set -e

ELAPSED="${SECONDS}s"

if [ $EXIT_CODE -eq 0 ]; then
# Extract framework-specific test counts for a compact summary
SUMMARY=""

# vitest: "Test Files 3 passed (3)"
if grep -qE "Test Files.*passed" "$TMPFILE" 2>/dev/null; then
COUNT=$(grep -oE "[0-9]+ passed" "$TMPFILE" | head -1 | awk '{print $1}')
[ -n "$COUNT" ] && SUMMARY="${COUNT} test files, "

# pytest: "8 passed in 1.23s"
elif grep -qE "[0-9]+ passed" "$TMPFILE" 2>/dev/null; then
COUNT=$(grep -oE "[0-9]+ passed" "$TMPFILE" | tail -1 | awk '{print $1}')
[ -n "$COUNT" ] && SUMMARY="${COUNT} tests, "
fi

printf "✓ %s (%s%s)\n" "$LABEL" "$SUMMARY" "$ELAPSED"
else
printf "✗ %s (exit %d, %s)\n" "$LABEL" "$EXIT_CODE" "$ELAPSED"
echo "---"
# Filter noise: ANSI codes, blank lines, node_modules frames, timing/cache lines
sed 's/\x1b\[[0-9;]*m//g' "$TMPFILE" \
| grep -v '^\s*$' \
| grep -v '^\s*at .*/node_modules/' \
| grep -v '^ Duration' \
| grep -v '^ Tasks:' \
| grep -v '^ Cache:' \
|| true
echo "---"
fi

exit $EXIT_CODE
Loading