Skip to content

Safety guards

Tilth runs autonomously for an hour or two at a time, against a real workspace, with shell access. The safety story is small but deliberate.

Hard stops

Tilth enforces a budget so an unattended run can't spin forever. The full mental model — which stop operates at which layer, what a hit looks like, how to resume past one — is in What can stop a run. The four tunable caps:

Cap Default Env
Iteration, per task 32 TILTH_MAX_ITERATIONS_PER_TASK
Wall-clock, per run 120 min TILTH_MAX_WALL_CLOCK_MINUTES
Dollar spend, per session $10.00 TILTH_MAX_TOKEN_DOLLAR_SPEND
Evaluator-call, per task (optional) 0 = off MAX_EVALUATOR_CALLS_PER_TASK

Two fixed circuit-breakers have no env knob — they guard against failures the caps can't see:

  • Provider-health gate. Every model call is checked against the provider's own signals (error object, finish_reason: "error", or a completely empty body). Unhealthy responses never enter the conversation — retried with the history untouched (8 attempts ≈ 3 minutes of backoff). Exhaustion aborts the task with provider_failure but leaves the session resumable. Worth a guard because unhealthy turns can cost nothing, so the dollar-spend cap never catches a bad endpoint.
  • No-case circuit breaker. A worker that keeps going quiet without calling submit_case is nudged up to 3 times, then the task aborts with no_case and the run halts.

Pre-tool veto

The pre_tool hook runs before every tool call and can block the call outright. Currently blocked patterns include:

  • git push --force / git push -f
  • git reset --hard
  • git clean -f (and force variants — -fd, -fdx, etc.)
  • sudo (including chained, e.g. ... && sudo ...)
  • curl ... | sh / wget ... | sh and friends
  • fork bombs

A blocked call surfaces as a tool-error feedback message to the agent on the next iteration. The agent learns "that doesn't work here" and chooses a different approach.

Worktree isolation

Every run creates a per-session git worktree on a session/<id> branch in your repo's git database. The working tree itself lives under ~/.tilth/sessions/<id>/workspace/ — see Session layout for the split. The file tools (read_file / write_file / edit_file) refuse paths that escape the worktree (tools/files.py:_resolve). The bash tool is not path-sandboxed, so treat the worktree boundary as a guard on the file tools, not on shell access — a determined cd .. reaches harness state (see the network/shell note below, and Agent visibility on the honest scope of the boundary).

The branch is never auto-merged. Open a PR and review like any other branch.

What's not a guard

A few honest gaps to be upfront about:

  • No spend cap where the provider reports no cost. The dollar-spend cap (TILTH_MAX_TOKEN_DOLLAR_SPEND) reads the provider's own per-call cost; a gateway that doesn't report one leaves spend at $0, so the cap never trips and wall-clock is the only backstop. (OpenRouter reports cost; other OpenAI-compatible gateways are untested here.) See Token recording → What it tracks, and what it still won't cap.
  • No headroom warning in the run itself. The caps the loop enforces are binary — under, run; at, stop; no "you're at 80%" alert mid-run. (The session visualizer does show amber/red utilization meters, but that's an observability surface, not an in-loop brake.)
  • No mid-task spend cut-off. Spend enforcement is between tasks. A single runaway task can overshoot its share by up to MAX_ITERATIONS_PER_TASK × cost_per_call. The trade-off is intentional; see Token recording → Enforcement.
  • No allow-listed network egress. Tools are intentionally narrow (no web fetch, no MCP, no curl-based downloads), so the network surface area is whatever the hard-coded shell guard misses. Treat the harness as having shell access and don't run it on machines you wouldn't run a foreign script on.