# Claude Code Hooks — Exercises

## Exercise 1: Write a Linting Hook

**Task:** Create a PostToolUse hook that runs a linter (e.g., ESLint, ShellCheck, or a language-appropriate linter) on files when the Edit or Write tool is used. Only run on files that match your project's conventions (e.g., `*.js`, `*.sh`).

**Validation:**
- [ ] Hook configured in `.claude/settings.json` under PostToolUse
- [ ] Hook checks `TOOL_NAME` and only runs for Edit/Write
- [ ] File path extracted from `TOOL_INPUT` (e.g., via `jq`)
- [ ] Linter runs on the edited file and results are written to a log file (not stdout)
- [ ] Hook has a reasonable timeout (e.g., 5000ms)

**Hints:**
1. Parse `TOOL_INPUT` with `jq`: `FILE=$(echo "$TOOL_INPUT" | jq -r '.file_path // empty')`
2. Check file extension before invoking the linter
3. Redirect linter output to a file: `eslint "$FILE" >> .local/lint.log 2>&1`

---

## Exercise 2: Write a Usage Tracking Hook

**Task:** Create a PostToolUse hook that increments a counter for each tool category in a JSON file (e.g., `.local/usage.json`). Structure: `{"edit": 5, "write": 3, "bash": 2}`. Use the hook's env vars to determine the tool.

**Validation:**
- [ ] Hook writes to a JSON file (create it if missing)
- [ ] Each tool use increments the correct key
- [ ] File is updated atomically (write to temp, then mv) to avoid corruption
- [ ] Hook exits 0 on success so it doesn't break the session

**Hints:**
1. Use `jq` to merge: `jq --arg cat "$TOOL_NAME" '.[$cat] = ((.[$cat] // 0) + 1)' file.json`
2. For atomic write: `jq ... file.json > file.json.tmp && mv file.json.tmp file.json`
3. Normalize tool name: `$(echo "$TOOL_NAME" | tr '[:upper:]' '[:lower:]')`

---

## Exercise 3: Write a Validation Hook

**Task:** Create a UserPromptSubmit hook that checks if the user's prompt contains a blocklisted word or pattern (e.g., "delete everything"). If detected, write a warning to a file and optionally append the prompt to an audit log. Do not block the agent — just log.

**Validation:**
- [ ] Hook reads `USER_PROMPT` from the environment
- [ ] Pattern matching implemented (grep, case statement, or simple `[[ == *pattern* ]]`)
- [ ] Warning written to a file when pattern matches
- [ ] Hook fails gracefully (handles empty `USER_PROMPT`, missing files)
- [ ] Hook has a short timeout (e.g., 2000ms)

**Hints:**
1. `if [[ "$USER_PROMPT" == *"delete everything"* ]]; then ...`
2. Write audit trail: `echo "$(date -Iseconds) $USER_PROMPT" >> .local/audit.log`
3. Use `set +e` or ensure script exits 0 even when pattern matches — you're logging, not blocking

---

## Exercise 4: Chain Multiple Hooks

**Task:** Configure at least two different hooks for the same project: (1) a PostToolUse hook that runs your linter, and (2) a Stop hook that logs token usage. Verify both run correctly in a single session.

**Validation:**
- [ ] PostToolUse hook runs during the session (check log/file output)
- [ ] Stop hook runs when the agent finishes (check log/file output)
- [ ] Both hooks are in `.claude/settings.json`
- [ ] Session completes without errors from the hooks

**Hints:**
1. Hooks are arrays — you can have multiple commands per event: `"PostToolUse": [{...}, {...}]`
2. Run a quick Edit in Claude Code, then check your hook output files
3. Ensure each script has a unique log file so you can distinguish them
