# Coding Agents & Hooks

Running multiple coding agents (Claude Code, Codex, or custom harnesses) across parallel Git worktrees is where laptops hit a wall: the agents themselves are lightweight HTTP loops, while the **verification tools they spawn** (`vitest`, `playwright`, `tsc --noEmit`, `eslint`, `cargo test`, `go test`) consume gigabytes of RAM per worktree.

`spotexec` integrates directly with coding agents through two mechanisms:

1. **Transparent command hooks** that rewrite heavy verification commands to `spotexec run -- '<cmd>'` automatically while leaving fast local commands untouched.
2. **Structured NDJSON telemetry** on non-TTY `stderr`, keeping the command's `stdout` and `stderr` clean for agent parsing.

## Installing the Claude Code Hook

```sh
# Install into .claude/settings.local.json (per-worktree / gitignored)
spotexec hook install --claude

# Or commit into .claude/settings.json (--shared) or install globally in ~/.claude (--global)
spotexec hook install --claude --shared
spotexec hook install --claude --global

# Dry-run any command string to verify whether the hook offloads or keeps it local
spotexec hook check 'pnpm vitest run'
spotexec hook check 'git status'
```

### What Gets Offloaded vs. Kept Local

The PreToolUse Bash hook inspects the command AST before execution:

```
Agent invokes Bash("<command>")
  │
  ├─ Project has .spotexec.toml? ── No ──▶ Run locally (no-op)
  │
  ├─ Prefixed with SPOTEXEC_LOCAL=1, or uses pipes / redirects / background `&` / `cd ..`?
  │   └─ Yes ──▶ Run locally
  │
  ├─ Matches local-only tools (git, rg, ls, dev servers, --watch)?
  │   └─ Yes ──▶ Run locally
  │
  ├─ Matches heavy verification (test suites, tsc, linters, formatters, builds, gradle/mvn verify)
  │  AND laptop normalized load >= when_load_above?
  │   └─ Yes ──▶ Rewrite to: spotexec run -- '<command>'
  │
  └─ Otherwise ──▶ Run locally
```

- **Offloaded to remote runners:** Unit & E2E test suites (`vitest`, `jest`, `playwright`, `pytest`, `go test`, `cargo test`), typecheckers (`tsc`), linters & formatters (`eslint`, `prettier`, `biome`, `ruff`, `clippy`), compilers/bundlers, and Maven/Gradle `verify` goals.
- **Kept local automatically:** `git`, `ls`, `rg`, `fd`, interactive dev servers (`next dev`, `vite dev`), watch modes (`--watch`), shell pipelines, file redirects, background jobs, commands that `cd` outside the repository, and any command prefixed with `SPOTEXEC_LOCAL=1`.

> [!TIP]
> If an integration test requires a service running on your laptop (such as `localhost:5432`), prefix that command with `SPOTEXEC_LOCAL=1 pnpm test:db` so the hook leaves it local.

### Permission Rules in Claude Code

The hook **never auto-approves commands**—it only rewrites the command string before Claude Code evaluates permissions. If you pre-approve test commands in Claude Code, add `Bash(spotexec run:*)` to your allowed tool list so rewritten commands run without manual prompts.

### Load-Based Offloading & `[hook]` Tuning

Configure `[hook]` in `.spotexec.toml` to offload commands only when your laptop is actually under load:

```toml
[hook]
when_load_above = 0.6          # offload only when normalized CPU load exceeds 60%
patterns = ["^pnpm build:"]    # additional regexes to offload
exclude = ["test:unit:fast"]   # regexes to always keep local
# mode = "block"               # deny direct execution with instructions instead of rewriting
# disabled = false             # disable the hook in this repository
```

## Codex and Other Agent Harnesses

For harnesses without a PreToolUse command-rewriting hook (such as Codex CLI), `spotexec` generates a concise, machine-targeted section in `AGENTS.md` instructing the agent when to prefix commands with `sx` and how to interpret exit code `125` vs test failures:

```sh
spotexec hook install --codex      # appends spotexec instructions to AGENTS.md
spotexec hook install --uninstall  # cleanly removes Claude or Codex hook configuration
```

## Machine-Readable NDJSON Telemetry

Whenever `stderr` is not a TTY—or when `--json` or `SPOTEXEC_AGENT=1` is set—`spotexec` replaces the interactive terminal spinner with compact, deterministic NDJSON lifecycle events on `stderr`. The remote command's own output streams untouched:

```json
{"event":"slot_acquired","connect_ms":13,"runner":"aws","slot":3,"waited_ms":0}
{"event":"sync_in","bytes":0,"duration_ms":0,"files":0,"mode":"fingerprint_hit"}
{"event":"sync_out","conflicts":[],"duration_ms":31,"mutated_files":["src/index.ts"]}
{"event":"result","duration_ms":4120,"exit_code":0,"timed_out":false}
```

Full lifecycle event types emitted for agents and log pipelines (`--events-file`):

- **Queueing & placement:** `queued`, `queue_position`, `placed`, `slot_acquired`
- **Elastic scaling:** `scale_out`, `provision`, `instance_running`, `instance_ready`
- **Execution pipeline:** `manifest`, `sync_in`, `deps`, `image`, `exec`, `sync_out`, `pull`, `cancel`, `result`
- **Spot resilience:** `spot_interruption`, `spot_rebalance`, `retry`
- **Diagnostics:** `warning` (with stable codes: `SYNC_CONFLICT`, `RUNNER_LOST`, `SPOT_INTERRUPTION`, `TIMEOUT`, `LOCAL_FALLBACK`) and `error` (`SPOTEXEC_ERROR`, paired with exit code `125`).
