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
195 changes: 195 additions & 0 deletions .claude/hooks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
# Claude Code Hooks for Safe Scientific Development

This directory contains hooks that provide technical enforcement of safety requirements for the `non_local_detector` project.

## Hook Overview

Hooks automatically intercept commands and verify safety requirements before execution. They provide the first layer of defense in the three-layer system (hooks + skills + documentation).

## Available Hooks

### pre-tool-use.sh
**Triggers:** Before every Bash tool execution

**Enforces:**
1. **Conda environment validation**
- Warns if Python commands run outside `non_local_detector` environment
- Suggests activation command
- Auto-prepends activation to commands

2. **Test-before-commit**
- Checks if tests have run recently before allowing commits
- Warns if no test cache found
- Recommends running test suite

3. **Snapshot update protection**
- Blocks `--snapshot-update` without approval flag
- Requires full analysis before approval
- Clears approval flag after use

**Exit codes:**
- `0`: Command allowed (may include warnings)
- `1`: Command blocked (e.g., snapshot update without approval)

---

### user-prompt-submit.sh
**Triggers:** After user submits a prompt

**Detects:**
- Snapshot file changes (`.ambr` files)
- Recent snapshot test runs

**Actions:**
- Reports modified snapshot files
- Reminds Claude to provide full analysis
- Lists requirements for snapshot update approval

**Exit codes:**
- Always `0` (informational only, never blocks)

---

## Utility Libraries

### lib/env_check.sh
**Functions:**
- `check_conda_env()`: Verify conda environment active
- `get_activation_cmd()`: Return activation command string
- `prepend_activation()`: Auto-prepend activation to command
- `needs_conda()`: Check if command requires conda environment

### lib/numerical_validation.sh
**Functions:**
- `check_invariants()`: Scan test output for NaN/Inf
- `has_snapshot_changes()`: Detect modified snapshot files
- `has_snapshot_approval()`: Check if approval flag set
- `set_snapshot_approval()`: Create approval flag file
- `clear_snapshot_approval()`: Remove approval flag file

---

## Hook Environment Variables

Hooks receive these environment variables from Claude Code:

- `CLAUDE_TOOL_NAME`: Name of tool being invoked (e.g., "Bash")
- `CLAUDE_TOOL_COMMAND`: Full command string being executed
- `CONDA_DEFAULT_ENV`: Current conda environment name

---

## Testing Hooks

### Manual testing

```bash
# Test environment check
export CLAUDE_TOOL_NAME="Bash"
export CLAUDE_TOOL_COMMAND="pytest --version"
.claude/hooks/pre-tool-use.sh
echo "Exit code: $?"

# Test snapshot protection (should block)
export CLAUDE_TOOL_COMMAND="pytest --snapshot-update"
.claude/hooks/pre-tool-use.sh
echo "Exit code: $?" # Should be 1

# Test with approval
source .claude/hooks/lib/numerical_validation.sh
set_snapshot_approval
.claude/hooks/pre-tool-use.sh
echo "Exit code: $?" # Should be 0
```

### Expected behavior

| Scenario | Hook | Expected Result |
|----------|------|-----------------|
| Python command, wrong env | pre-tool-use | Warning (exit 0) |
| Commit without recent tests | pre-tool-use | Warning (exit 0) |
| Snapshot update, no approval | pre-tool-use | Blocked (exit 1) |
| Snapshot update, with approval | pre-tool-use | Allowed, flag cleared (exit 0) |
| Snapshot files modified | user-prompt-submit | Info message (exit 0) |

---

## Integration with Skills

Hooks work together with skills:

1. **Skills** (layer 2) guide Claude through workflows
2. **Hooks** (layer 1) enforce critical requirements
3. **Documentation** (layer 3) explains the "why"

Example flow:
```
Claude uses scientific-tdd skill
Skill says: "Run tests"
Claude executes: pytest command
pre-tool-use hook checks conda env
Hook passes or warns
Command executes
```

---

## Debugging Hooks

If hooks misbehave:

1. **Check hook permissions:**
```bash
ls -l .claude/hooks/*.sh
# Should show -rwxr-xr-x
```

2. **Test utilities directly:**
```bash
source .claude/hooks/lib/env_check.sh
check_conda_env && echo "OK" || echo "FAIL"
```

3. **Run hook with debug output:**
```bash
bash -x .claude/hooks/pre-tool-use.sh
```

4. **Check environment variables:**
```bash
echo "Tool: $CLAUDE_TOOL_NAME"
echo "Command: $CLAUDE_TOOL_COMMAND"
echo "Conda: $CONDA_DEFAULT_ENV"
```

---

## Maintenance

### When to update hooks:

- Conda environment name changes
- New tools need environment checking
- Validation requirements change
- New safety checks needed

### Best practices:

- Keep hooks fast (< 100ms)
- Always exit with appropriate code (0 or 1)
- Provide helpful error messages
- Source utilities from `lib/` directory
- Test thoroughly before deployment

---

## Related Documentation

- **CLAUDE.md**: Project operational rules
- **Skills**: Workflow enforcement
- **Testing Plan**: Coverage improvement strategy
48 changes: 48 additions & 0 deletions .claude/hooks/lib/env_check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/bin/bash
# Environment validation utilities for non_local_detector project

# Check if conda environment is activated
check_conda_env() {
local required_env="non_local_detector"

# Check CONDA_DEFAULT_ENV
if [ "$CONDA_DEFAULT_ENV" = "$required_env" ]; then
return 0
fi

# Check python path
local python_path=$(which python 2>/dev/null)
if [[ "$python_path" == *"/envs/$required_env/"* ]]; then
return 0
fi

return 1
}

# Get activation command
get_activation_cmd() {
echo "conda activate non_local_detector"
}

# Auto-prepend conda activation to command
prepend_activation() {
local cmd="$1"
echo "conda activate non_local_detector && $cmd"
}

# Check if command needs conda environment
needs_conda() {
local cmd="$1"

# Check for Python-related commands
if [[ "$cmd" =~ ^(python|pytest|pip|black|ruff|mypy) ]]; then
return 0
fi

# Check for full paths to conda binaries
if [[ "$cmd" =~ /envs/[^/]+/bin/ ]]; then
return 0
fi

return 1
}
49 changes: 49 additions & 0 deletions .claude/hooks/lib/numerical_validation.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/bin/bash
# Numerical validation utilities for scientific code

# Check if mathematical invariants hold
check_invariants() {
local test_output="$1"

# Look for common numerical issues in test output
if echo "$test_output" | grep -q "NaN\|Inf\|-Inf"; then
echo "❌ Numerical issue detected: NaN or Inf in outputs"
return 1
fi

return 0
}

# Detect snapshot changes
has_snapshot_changes() {
# Check if any snapshot files have been modified
if git status --porcelain | grep -q "\.ambr$"; then
return 0
fi

# Check syrupy snapshot directory
if [ -d ".pytest_cache/v/cache/snapshot" ]; then
if [ -n "$(find .pytest_cache/v/cache/snapshot -mmin -5 2>/dev/null)" ]; then
return 0
fi
fi

return 1
}

# Check if approval flag is set
has_snapshot_approval() {
[ -f ".claude/snapshot_update_approved" ]
}

# Set approval flag
set_snapshot_approval() {
mkdir -p .claude
touch .claude/snapshot_update_approved
echo "Snapshot update approved at $(date)" > .claude/snapshot_update_approved
}

# Clear approval flag
clear_snapshot_approval() {
rm -f .claude/snapshot_update_approved
}
77 changes: 77 additions & 0 deletions .claude/hooks/pre-tool-use.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/bin/bash
# Pre-tool-use hook for Claude Code
# Enforces conda environment and test-before-commit requirements

# Source utilities
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/env_check.sh"
source "$SCRIPT_DIR/lib/numerical_validation.sh"

# Get the tool name and command from arguments
TOOL_NAME="${CLAUDE_TOOL_NAME:-unknown}"
TOOL_COMMAND="${CLAUDE_TOOL_COMMAND:-}"

# Only process Bash tool invocations
if [ "$TOOL_NAME" != "Bash" ]; then
exit 0
fi

# Extract base command (first word)
BASE_CMD=$(echo "$TOOL_COMMAND" | awk '{print $1}')

# Check 1: Environment enforcement for Python commands
if needs_conda "$BASE_CMD"; then
if ! check_conda_env; then
echo "❌ Wrong conda environment detected!"
echo "Required: non_local_detector"
echo "Current: ${CONDA_DEFAULT_ENV:-none}"
echo ""
echo "Run: $(get_activation_cmd)"
echo ""
echo "Or commands will be auto-prepended with activation."

# Don't block - just warn (auto-prepend will handle it)
exit 0
fi
fi

# Check 2: Test-before-commit enforcement
if [[ "$TOOL_COMMAND" =~ ^git[[:space:]]+commit ]]; then
# Check if tests have run recently (within last 5 minutes)
if [ -d ".pytest_cache" ]; then
CACHE_AGE=$(find .pytest_cache -type f -mmin -5 2>/dev/null | wc -l)
if [ "$CACHE_AGE" -eq 0 ]; then
echo "⚠️ No recent test runs detected"
echo "Recommendation: Run tests before committing"
echo ""
echo "Run: /Users/edeno/miniconda3/envs/non_local_detector/bin/pytest -v"
echo ""
echo "Proceeding with commit anyway (warning only)..."
fi
fi
fi

# Check 3: Snapshot update protection
if [[ "$TOOL_COMMAND" =~ --snapshot-update ]]; then
if ! has_snapshot_approval; then
echo "❌ Snapshot update attempted without approval!"
echo ""
echo "Snapshot updates require explicit approval."
echo "Claude must provide full analysis first:"
echo " 1. Diff: What changed in snapshots"
echo " 2. Explanation: Why the change occurred"
echo " 3. Validation: Mathematical properties still hold"
echo " 4. Test case: Demonstrate correctness"
echo ""
echo "After approval, user must set approval flag."

# Block this command
exit 1
fi

# Clear approval flag after use
clear_snapshot_approval
fi

# All checks passed
exit 0
Loading
Loading