From d63aba788ee3842740bd4b5685a400b9e54cf57c Mon Sep 17 00:00:00 2001 From: Dorian Date: Tue, 3 Mar 2026 17:45:21 +0000 Subject: [PATCH] chore(.gitignore): add loop log file to ignore list --- .claude/hooks/block-risky-bash.sh | 56 +++++++++++ .claude/hooks/protect-files.sh | 80 +++++++++++++++ .claude/settings.json | 24 +++++ .claude/worktrees/agitated-hofstadter | 1 + .claude/worktrees/funny-hofstadter | 1 + .claude/worktrees/happy-colden | 1 + .claude/worktrees/hardcore-beaver | 1 + .claude/worktrees/heuristic-raman | 1 + .claude/worktrees/priceless-colden | 1 + .gitignore | 3 + loop/README.md | 138 ++++++++++++++++++++++++++ loop/loop.sh | 43 ++++++++ loop/plan.md | 38 +++++++ loop/prepare.sh | 41 ++++++++ loop/prompt.md | 1 + 15 files changed, 430 insertions(+) create mode 100755 .claude/hooks/block-risky-bash.sh create mode 100755 .claude/hooks/protect-files.sh create mode 100644 .claude/settings.json create mode 160000 .claude/worktrees/agitated-hofstadter create mode 160000 .claude/worktrees/funny-hofstadter create mode 160000 .claude/worktrees/happy-colden create mode 160000 .claude/worktrees/hardcore-beaver create mode 160000 .claude/worktrees/heuristic-raman create mode 160000 .claude/worktrees/priceless-colden create mode 100644 loop/README.md create mode 100755 loop/loop.sh create mode 100644 loop/plan.md create mode 100755 loop/prepare.sh create mode 100644 loop/prompt.md diff --git a/.claude/hooks/block-risky-bash.sh b/.claude/hooks/block-risky-bash.sh new file mode 100755 index 00000000..5eb6bbaf --- /dev/null +++ b/.claude/hooks/block-risky-bash.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# PreToolUse Bash guard: block dangerous shell commands. +# Denies: rm -rf, git reset --hard, git push -f, git clean -fd, chmod -R 777, +# fork bombs, block device overwrites, mkfs, paths escaping project root. +set -euo pipefail + +INPUT=$(cat) +CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty') +BASE="${CLAUDE_PROJECT_DIR:-}" +[[ -z "$BASE" ]] && BASE=$(echo "$INPUT" | jq -r '.cwd // empty') +[[ -z "$BASE" ]] && BASE="$(pwd)" + +# Normalize: collapse whitespace, strip leading/trailing +CMD_NORM=$(echo "$CMD" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + +deny() { + local reason="$1" + jq -n --arg r "$reason" '{ + hookSpecificOutput: { + hookEventName: "PreToolUse", + permissionDecision: "deny", + permissionDecisionReason: $r + } + }' + exit 0 +} + +# Dangerous patterns (case-insensitive where sensible) +case "$CMD_NORM" in + *"rm -rf"*|*"rm -fr"*|*"rm -f -r"*|*"rm -r -f"*) deny "Destructive rm -rf blocked by security hook" ;; + *"git reset --hard"*) deny "git reset --hard would lose uncommitted work" ;; + *"git push --force"*|*"git push -f"*|*"git push -f "*) deny "git push --force would rewrite history" ;; + *"git clean -fd"*|*"git clean -f -d"*) deny "git clean -fd deletes untracked files" ;; + *"chmod -R 777"*|*"chmod -R 0777"*) deny "chmod -R 777 is a security risk" ;; + *":(){ :"*"};:"*) deny "Fork bomb pattern blocked" ;; + *"> /dev/sd"*|*">/dev/sd"*) deny "Block device overwrite blocked" ;; + *"mkfs "*|*"mkfs."*) deny "Disk format command blocked" ;; +esac + +# Check for path traversal escaping project root (../ outside project) +# Only if we have a sensible base +if [[ -n "$BASE" ]] && [[ -d "$BASE" ]]; then + ABS_BASE=$(cd "$BASE" && pwd) + # Simple heuristic: command contains .. and would resolve outside project + if echo "$CMD_NORM" | grep -qE '\.\./|/\.\.'; then + # Extract plausible paths and check - allow ../ within project + if echo "$CMD_NORM" | grep -qE '(rm|mv|cp|cat|chmod|chown)\s+.*\.\.'; then + # Could be risky; be conservative for rm/mv/cp + if echo "$CMD_NORM" | grep -qE '\brm\b.*\.\.'; then + deny "Path traversal with rm blocked" + fi + fi + fi +fi + +exit 0 diff --git a/.claude/hooks/protect-files.sh b/.claude/hooks/protect-files.sh new file mode 100755 index 00000000..c8ac2211 --- /dev/null +++ b/.claude/hooks/protect-files.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# PreToolUse Edit|Write guard: block edits outside project and to protected paths. +# Denies: paths outside project, .git/, .env*, lockfiles, node_modules/ +set -euo pipefail + +INPUT=$(cat) +FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') +BASE="${CLAUDE_PROJECT_DIR:-}" +[[ -z "$BASE" ]] && BASE=$(echo "$INPUT" | jq -r '.cwd // empty') +[[ -z "$BASE" ]] && BASE="$(pwd)" + +# Resolve to absolute path +if [[ -z "$FILE_PATH" ]]; then + exit 0 +fi +ABS_BASE=$(cd "$BASE" 2>/dev/null && pwd) || true +[[ -z "$ABS_BASE" ]] && ABS_BASE=$(python3 -c "import os,sys; print(os.path.abspath(os.path.normpath(sys.argv[1])))" "$BASE" 2>/dev/null) || true +[[ -z "$ABS_BASE" ]] && ABS_BASE="$BASE" +# Ensure base has trailing slash for prefix check +[[ "$ABS_BASE" != */ ]] && ABS_BASE="${ABS_BASE}/" +if [[ "$FILE_PATH" != /* ]]; then + ABS_PATH="$ABS_BASE${FILE_PATH#./}" +else + ABS_PATH="$FILE_PATH" +fi +# Normalize path (collapse .. and ., no symlink resolution needed) +ABS_PATH=$(python3 -c "import os,sys; print(os.path.abspath(os.path.normpath(sys.argv[1])))" "$ABS_PATH" 2>/dev/null) || true +[[ -z "$ABS_PATH" ]] && ABS_PATH="$ABS_BASE${FILE_PATH#./}" + +# Protected patterns (path contains or equals) +PROTECTED_PATTERNS=( + ".git/" + ".env" + ".env.local" + "node_modules/" + "package-lock.json" + "pnpm-lock.yaml" +) + +for pattern in "${PROTECTED_PATTERNS[@]}"; do + if [[ "$ABS_PATH" == *"$pattern"* ]] || [[ "$ABS_PATH" == *"/$pattern" ]]; then + echo "Blocked: $ABS_PATH matches protected pattern '$pattern'" >&2 + jq -n --arg r "Edit blocked: path matches protected pattern ($pattern)" '{ + hookSpecificOutput: { + hookEventName: "PreToolUse", + permissionDecision: "deny", + permissionDecisionReason: $r + } + }' + exit 0 + fi +done + +# .env.*.local +if [[ "$ABS_PATH" =~ \.env\..*\.local$ ]]; then + echo "Blocked: $ABS_PATH is an env secrets file" >&2 + jq -n '{ + hookSpecificOutput: { + hookEventName: "PreToolUse", + permissionDecision: "deny", + permissionDecisionReason: "Edit blocked: .env.*.local files contain secrets" + } + }' + exit 0 +fi + +# Ensure path is under project root (ABS_BASE has trailing /) +if [[ "$ABS_PATH" != "$ABS_BASE"* ]] && [[ "$ABS_PATH" != "$BASE"* ]]; then + echo "Blocked: $ABS_PATH is outside project ($ABS_BASE)" >&2 + jq -n --arg r "Edit blocked: path is outside project directory" '{ + hookSpecificOutput: { + hookEventName: "PreToolUse", + permissionDecision: "deny", + permissionDecisionReason: $r + } + }' + exit 0 +fi + +exit 0 diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000..73207c49 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,24 @@ +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-risky-bash.sh" + } + ] + }, + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh" + } + ] + } + ] + } +} diff --git a/.claude/worktrees/agitated-hofstadter b/.claude/worktrees/agitated-hofstadter new file mode 160000 index 00000000..10e12a32 --- /dev/null +++ b/.claude/worktrees/agitated-hofstadter @@ -0,0 +1 @@ +Subproject commit 10e12a329f0652a2270d2b988eae42567b792cef diff --git a/.claude/worktrees/funny-hofstadter b/.claude/worktrees/funny-hofstadter new file mode 160000 index 00000000..721e915a --- /dev/null +++ b/.claude/worktrees/funny-hofstadter @@ -0,0 +1 @@ +Subproject commit 721e915a48328edafd8c53e8bc52a2bd725d294e diff --git a/.claude/worktrees/happy-colden b/.claude/worktrees/happy-colden new file mode 160000 index 00000000..721e915a --- /dev/null +++ b/.claude/worktrees/happy-colden @@ -0,0 +1 @@ +Subproject commit 721e915a48328edafd8c53e8bc52a2bd725d294e diff --git a/.claude/worktrees/hardcore-beaver b/.claude/worktrees/hardcore-beaver new file mode 160000 index 00000000..721e915a --- /dev/null +++ b/.claude/worktrees/hardcore-beaver @@ -0,0 +1 @@ +Subproject commit 721e915a48328edafd8c53e8bc52a2bd725d294e diff --git a/.claude/worktrees/heuristic-raman b/.claude/worktrees/heuristic-raman new file mode 160000 index 00000000..721e915a --- /dev/null +++ b/.claude/worktrees/heuristic-raman @@ -0,0 +1 @@ +Subproject commit 721e915a48328edafd8c53e8bc52a2bd725d294e diff --git a/.claude/worktrees/priceless-colden b/.claude/worktrees/priceless-colden new file mode 160000 index 00000000..721e915a --- /dev/null +++ b/.claude/worktrees/priceless-colden @@ -0,0 +1 @@ +Subproject commit 721e915a48328edafd8c53e8bc52a2bd725d294e diff --git a/.gitignore b/.gitignore index 48ce15ff..a023a0e2 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,9 @@ pnpm-debug.log* # Test coverage coverage/ +# Overnight loop logs +loop/loop.log + # Playwright test-results/ playwright-report/ diff --git a/loop/README.md b/loop/README.md new file mode 100644 index 00000000..aa8ae25b --- /dev/null +++ b/loop/README.md @@ -0,0 +1,138 @@ +# Overnight Claude Automation + +Run Claude Code autonomously while you're away. Combines sleep prevention, task-based execution, the Ralph Wiggum Technique (Stop hook blocks until plan is complete), and security hooks that restrict AI to project files and block destructive commands. + +## Prerequisites + +- **Claude Code CLI** ([claude.ai/code](https://claude.ai/code)) — installed at `~/.local/bin/claude` or in PATH +- **Hooks** — user-level hooks in `~/.claude/` (sleep, Ralph Wiggum) +- **jq** — for security hook scripts (`brew install jq`) + +## Flow + +### Pre-run (before 5–6pm) + +1. **Commit and push** — Snap current work and back up to remote. +2. **Run prepare script** — Creates date-stamped branch and verifies clean state: + + ```bash + ./loop/prepare.sh + ``` + +3. **Edit plan** — Update `loop/plan.md` with evening scope and tasks (see template below). +4. **Commit plan** — Version the plan so you can revert if needed: + + ```bash + git add loop/plan.md && git commit -m "chore: overnight plan $(date +%Y-%m-%d)" + ``` + +5. **Push** (optional but recommended): `git push -u origin overnight/YYYY-MM-DD` + +### Overnight + +```bash +tmux new -s overnight +caffeinate -i ./loop/loop.sh +# Detach: Ctrl+B, then D +``` + +### Post-run (next morning) + +1. `git status` and `git diff` to review changes. +2. Run `pnpm test && pnpm lint && pnpm typecheck`. +3. Merge branch or revert if needed. + +## Quick Start + +1. **Edit your plan** — Add tasks to `loop/plan.md` using the evening run format: + + ```markdown + # Evening Run — YYYY-MM-DD + + ## Scope + Add tests to chat components. + + ## Tasks + - [ ] Add unit tests for useAI composable + - [ ] Fix linter errors in packages/app + ``` + +2. **Run overnight** — From project root: + + ```bash + caffeinate -i ./loop/loop.sh + ``` + +## How It Works + +| Component | Purpose | +|-----------|---------| +| **UserPromptSubmit hook** | Starts `caffeinate` to prevent Mac sleep when you submit a prompt | +| **Stop hook** | Checks `plan.md` for unchecked tasks; blocks Claude from stopping until all are done (Ralph Wiggum) | +| **SessionEnd hook** | Kills `caffeinate` so Mac can sleep again | +| **PreToolUse (Bash)** | Blocks dangerous commands (rm -rf, git reset --hard, etc.) | +| **PreToolUse (Edit\|Write)** | Blocks edits outside project and to protected paths | +| **loop.sh** | Runs Claude with `--dangerously-skip-permissions` and feeds the prompt from `loop/prompt.md` | + +## Security Model + +Project-scoped hooks in `.claude/hooks/` restrict the AI during overnight runs: + +### Bash guard (`block-risky-bash.sh`) + +Blocks: `rm -rf`, `git reset --hard`, `git push --force`, `git clean -fd`, `chmod -R 777`, fork bombs, block device overwrites, `mkfs`, and path traversal with destructive commands. + +### File edit guard (`protect-files.sh`) + +Blocks Edit/Write when: + +- Path is **outside project directory** +- Path contains **`.git/`** +- Path is **`.env`**, **`.env.local`**, **`.env.*.local`** +- Path is **`package-lock.json`** or **`pnpm-lock.yaml`** +- Path contains **`node_modules/`** + +Read, Glob, and Grep remain unrestricted. + +## Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `CLAUDE_AUTONOMOUS` | `1` | Set to `1` to enable Ralph Wiggum (Stop hook checks plan). `0` disables. | +| `CLAUDE_PLAN_FILE` | `plan.md` | Plan file path (relative to project). | +| `ITERATION_COUNT` | `1` | Number of loop iterations (use >1 for multi-run without Ralph Wiggum). | +| `ITERATION_DELAY` | `600` | Seconds between iterations when `ITERATION_COUNT` > 1. | +| `PROMPT_FILE` | `loop/prompt.md` | Prompt content for Claude. | +| `LOG_FILE` | `loop/loop.log` | Log output (gitignored). | + +## Scheduling (Optional) + +Install [claude-code-schedule](https://github.com/macalinao/claude-code-schedule) for time-based runs: + +```bash +cargo install claude-code-schedule +ccschedule --time 05:30 --message "Review plan.md and complete next task" +``` + +## continuous-claude (Optional) + +For full PR-based workflow (branches, PRs, CI): + +```bash +# Install from https://github.com/AnandChowdhary/continuous-claude +continuous-claude -p "Work through loop/plan.md" -m 10 --max-duration 8h +``` + +## Remote Monitoring + +- **Tmux + SSH**: Attach from another machine: `ssh host 'tmux attach -t overnight'` +- **Tailscale**: Use Tailscale for easy remote SSH when away from home network +- **Log tail**: `tail -f loop/loop.log` to watch progress + +## Safety + +- **Start small** — Test with 1–2 tasks before overnight runs +- **prepare.sh** — Run before starting; fails if working tree is dirty or branch exists +- **Git** — Loop does not auto-commit; you review and merge in the morning +- **`--dangerously-skip-permissions`** — Security hooks still run and block dangerous actions +- **Project-scoped hooks** — Only apply when Claude runs in AIUI; other projects unaffected diff --git a/loop/loop.sh b/loop/loop.sh new file mode 100755 index 00000000..78c0de84 --- /dev/null +++ b/loop/loop.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env sh +# Headless loop script for overnight Claude Code automation. +# Set CLAUDE_AUTONOMOUS=1 for Ralph Wiggum (Stop hook blocks until plan is complete). +set -eu + +PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "$0")/.." && pwd)}" +PROMPT_FILE="${PROMPT_FILE:-$PROJECT_DIR/loop/prompt.md}" +LOG_FILE="${LOG_FILE:-$PROJECT_DIR/loop/loop.log}" +ITERATION_COUNT="${ITERATION_COUNT:-1}" +ITERATION_DELAY="${ITERATION_DELAY:-600}" +CLAUDE_BIN="${CLAUDE_BIN:-claude}" + +cd "$PROJECT_DIR" + +echo "=== Overnight loop started $(date '+%Y-%m-%dT%H:%M:%S' 2>/dev/null || date) ===" | tee -a "$LOG_FILE" +echo " PROMPT_FILE=$PROMPT_FILE" | tee -a "$LOG_FILE" +echo " CLAUDE_AUTONOMOUS=${CLAUDE_AUTONOMOUS:-0}" | tee -a "$LOG_FILE" +echo " ITERATION_COUNT=$ITERATION_COUNT" | tee -a "$LOG_FILE" +echo "" | tee -a "$LOG_FILE" + +i=1 +while [ "$i" -le "$ITERATION_COUNT" ]; do + echo "--- Iteration $i/$ITERATION_COUNT @ $(date '+%Y-%m-%dT%H:%M:%S' 2>/dev/null || date) ---" | tee -a "$LOG_FILE" + export CLAUDE_PROJECT_DIR="$PROJECT_DIR" + export CLAUDE_AUTONOMOUS="${CLAUDE_AUTONOMOUS:-1}" + + # Run Claude with autonomous permissions; prompt from file + if [ -f "$PROMPT_FILE" ]; then + "$CLAUDE_BIN" -p --dangerously-skip-permissions --output-format=stream-json \ + < "$PROMPT_FILE" 2>&1 | tee -a "$LOG_FILE" || true + else + echo "Error: $PROMPT_FILE not found" | tee -a "$LOG_FILE" + exit 1 + fi + + i=$((i + 1)) + if [ "$i" -le "$ITERATION_COUNT" ] && [ "$ITERATION_DELAY" -gt 0 ]; then + echo "Waiting ${ITERATION_DELAY}s before next iteration..." | tee -a "$LOG_FILE" + sleep "$ITERATION_DELAY" + fi +done + +echo "=== Loop complete $(date '+%Y-%m-%dT%H:%M:%S' 2>/dev/null || date) ===" | tee -a "$LOG_FILE" diff --git a/loop/plan.md b/loop/plan.md new file mode 100644 index 00000000..1ca42e7e --- /dev/null +++ b/loop/plan.md @@ -0,0 +1,38 @@ +# Evening Run — YYYY-MM-DD + +> **Format**: Use `- [ ]` for incomplete, `- [x]` for complete. Enable autonomous mode with `CLAUDE_AUTONOMOUS=1` when running. + +## Scope + +Get the AIUI project to a green CI baseline: ESLint config, initial test coverage, and passing `pnpm test`, `pnpm lint`, `pnpm typecheck`. Work incrementally; complete as many tasks as possible. + +## Pre-run checklist (you complete before 5–6pm) + +- [ ] Committed all work; `git status` clean +- [ ] Pushed to remote (or at least backed up) +- [ ] Created branch for overnight: `git checkout -b overnight/YYYY-MM-DD` + +## Instructions for Claude + +Work through the checklist below. Complete one item per iteration, then update this file to mark it done. The Stop hook will keep you running until all tasks are checked. If a task is blocked or you hit a natural stopping point, add context to the Notes section and continue with the next task. + +## Tasks + +- [ ] Add `eslint.config.js` (flat config) to `packages/app` for ESLint 10 — extend recommended Vue/TypeScript rules +- [ ] Add `eslint.config.js` (flat config) to `packages/core` — same pattern as app +- [ ] Add a minimal smoke test in `packages/core` (e.g. `src/index.test.ts`) so `pnpm test` no longer fails with "No test files found" +- [ ] Add unit tests for `useTheme` composable in `packages/app` — test theme detection and toggle behavior +- [ ] Run `pnpm lint` and fix any reported issues (or document known unfixable ones in Notes) +- [ ] Add unit tests for `useAI` or `contentFiltering` composable — at least one meaningful test +- [ ] Verify `pnpm test`, `pnpm lint`, `pnpm typecheck` all pass; fix failures +- [ ] Update this plan's Success criteria section — mark items that pass + +## Success criteria + +- [ ] pnpm test passes +- [ ] pnpm lint passes +- [ ] No new linter/type errors + +## Notes + +(Claude will add context here for the next iteration.) diff --git a/loop/prepare.sh b/loop/prepare.sh new file mode 100755 index 00000000..d0066f17 --- /dev/null +++ b/loop/prepare.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env sh +# Pre-run script: verify repo state and create overnight branch. +# Run before 5-6pm to ensure you can safely start the overnight loop. +set -eu + +PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "$0")/.." && pwd)}" +cd "$PROJECT_DIR" + +DATE=$(date '+%Y-%m-%d') +BRANCH="overnight/${DATE}" + +echo "=== Overnight pre-run check @ $(date '+%Y-%m-%dT%H:%M:%S') ===" + +# 1. Check git status is clean +if ! git diff --quiet || ! git diff --cached --quiet; then + echo "Error: Working tree not clean. Commit or stash changes first." >&2 + echo " git status" >&2 + git status --short >&2 + exit 1 +fi + +# 2. Check we're not already on an overnight branch +current=$(git branch --show-current 2>/dev/null || true) +if [ -n "$current" ] && [ "$current" = "$BRANCH" ]; then + echo "Already on $BRANCH. Ready to run." >&2 + exit 0 +fi + +# 3. Create date-stamped branch +if git rev-parse --verify "$BRANCH" >/dev/null 2>&1; then + echo "Branch $BRANCH already exists. Checkout or use a different date." >&2 + exit 1 +fi +git checkout -b "$BRANCH" +echo "Created branch $BRANCH" + +# 4. Remind to push +echo "" +echo "Reminder: Push before starting overnight run: git push -u origin $BRANCH" +echo "Then run: caffeinate -i ./loop/loop.sh" +echo "=== Ready ===" diff --git a/loop/prompt.md b/loop/prompt.md new file mode 100644 index 00000000..94332822 --- /dev/null +++ b/loop/prompt.md @@ -0,0 +1 @@ +Read `loop/plan.md` and work on the next incomplete task. Update the plan to mark completed items with `- [x]`. Add any relevant context to the Notes section for the next iteration. Do not stop until all tasks in the plan are complete.