> ## Documentation Index
> Fetch the complete documentation index at: https://smithers.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Common Footguns

> The handful of mistakes that bite people first, and the pattern that avoids each.

Smithers is durable by design: a run replays from persisted state, not memory, and that's the source of most of the sharp edges below.

## Resume and state

### Unstable task IDs break resume

The runtime keys completed work by task `id`: a changed id looks like a new task, a disappeared one drops from the plan. Derive ids from data, never a loop index or timestamp.

```tsx theme={"theme":{"light":"github-light","dark":"github-dark"}}
{tickets.map((t) => <TicketPipeline key={t.id} id={`${t.id}:work`} />)}
// NOT id={`work-${i}`} or id={`work-${Date.now()}`}
```

Same rule as React keys; see [How It Works](/how-it-works).

### Input is immutable after the first run

A run's `--input` is persisted at start; resuming with different input errors rather than overriding it silently. Start a new run instead.

### Code changes block resume, they do not merge

A workflow source change is a different workflow: resume checks the source hash, so editing the file blocks it. Start a new run, or hot-reload (`up --hot`) a still-running one: edits apply to newly scheduled tasks while in-flight tasks finish on their original code. See [Recipes](/recipes).

### `useState` is not durable

React state resets every render, i.e. every frame here. Anything that must survive a crash belongs in a Task output read back through `ctx`, not component state.

### Evidence files must be run-scoped or cleaned

If a workflow writes verdict files like `artifacts/.../verify.json` and reads them back for a done-check, put `ctx.runId` in the path, or delete the evidence directory at run start: a prior run's `verify.json` must never satisfy a fresh run's done-check.

## Caching

### Do not cache side-effecting tasks

`cache` is for pure, expensive-to-recompute work: caching a deploy, email, or mutation means it silently skips on a hit. The key is `cache.by(ctx)` plus `cache.version` plus the output schema signature, so a schema change auto-invalidates it, and a stale cached row fails validation, missing safely. See [How It Works](/how-it-works).

## Side effects and retries

### Mark side-effecting tools and key them

Tasks retry, and a retried tool call can fire twice: declare `sideEffect: true` on a custom tool and pass `ctx.idempotencyKey` through to the downstream system so a retry is a no-op, not a second charge. The key stays stable across retries and resumes for the same task iteration.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { defineTool } from "smithers-orchestrator";
import { z } from "zod";

const placeOrder = defineTool({
  name: "shop.place_order",
  description: "Place an order",
  schema: z.object({ sku: z.string() }),
  sideEffect: true,
  idempotent: false,
  async execute(args, ctx) {
    return await shop.placeOrder({ sku: args.sku, idempotencyKey: ctx.idempotencyKey });
  },
});
```

### Decide in one task, act in another

Marking and keying the tool stops double-charging on retry but doesn't make the
charge reviewable: an agent that decides to send money and sends it can't
have that decision inspected or rerun without risking a second charge. Have the
deciding task return a typed decision (`{ shouldPay, amount, reason }`) and put
the payout in its own downstream task behind an
[`<Approval>`](/components/approval): the decision stays reversible and
replayable, the act isolated, gated, and keyed. See [Sequence for
reversibility](/guides/context-engineering#sequence-for-reversibility-isolate-the-irreversible)
for the full pattern.

## Tools and sandbox

### Agents get only the tools you grant

The five built-in tools (`read`, `write`, `edit`, `grep`, `bash`) are sandboxed to `rootDir`: symlinks, network, and long-running calls are denied by default (`--allow-network` opens bash to the network). Grant least privilege per task: a reviewer gets `read`/`grep`, an implementer gets `write`/`edit`/`bash`, and an agent with no `tools` can't touch the filesystem at all. See [How It Works](/how-it-works).

## Autonomous and detached runs

### Agents need permission-bypass flags or a detached run hangs

A `ClaudeCodeAgent` or `CodexAgent` built with just a `model` prompts for
permission before editing files: fine interactively, but in a detached run
(`up -d`) nothing clicks to approve it, so the task stalls until its heartbeat
timeout. Construct autonomous-run agents with the bypass flags:

```tsx theme={"theme":{"light":"github-light","dark":"github-dark"}}
const opus = new ClaudeCodeAgent({
  model: "claude-opus-5",
  permissionMode: "bypassPermissions",
  dangerouslySkipPermissions: true,
});
const codex = new CodexAgent({
  model: "gpt-5.6-luna",
  config: { model_reasoning_effort: "medium" },
  sandbox: "danger-full-access",
  dangerouslyBypassApprovalsAndSandbox: true,
  skipGitRepoCheck: true,
});
```

`.smithers/agents.ts`'s named pools are intentionally bypass-free for
interactive use; don't reach for them in a detached workflow without adding the
flags.

### `--hot` is for a live process, not for resuming after edits

Hot reload (`up --hot`) applies workflow and prompt edits on the next render
frame of a live process; it can't retroactively unblock a suspended run, like
one paused at an approval gate. Resume still rejects an
edited file, now with `RESUME_METADATA_MISMATCH`; changing task IDs or the
module graph always needs a fresh run. To iterate on a detached run stuck at a
gate, keep a live `up --hot` process attached, or start a new run and gate
finished phases off with an input flag to skip them.

### Never pin `cwd` on an agent you use inside `<Worktree>`

A pinned `cwd` takes precedence over the per-task root: the agent reads,
writes, and commits to the launch directory's base branch instead of its
worktree. Leave `cwd` unset and let `<Worktree>` (or the launch root) control
the directory; the engine logs a "pinned cwd overrides Worktree" warning when
you get this wrong.

## Time travel and VCS

### Revert and VCS-restoring replay change your working tree

Both rewrite filesystem state; treat them like `git checkout` over uncommitted work.

* `revert` restores the workspace to a previous attempt's filesystem state, discarding graph snapshots recorded after it; it touches files only, landing as a new change atop the current working copy. See [Revert to Attempt](/runtime/revert).
* `replay --restore-vcs` checks out the jj revision the snapshot was taken at, so re-execution sees the original run's source.

### `revert` requires jj

Smithers prefers `.jj` over `.git`: pure Git repos run fine but can't use `revert`, since there's no per-attempt change to restore. Install jj for attempt-level revert. See [VCS](/guides/vcs).

### Worktree runs auto-rebase on resume

On resume, Smithers rebases a worktree run onto the base branch (default `main`), continuing even if the rebase fails: expect the branch to move.

### `<Worktree baseBranch>` must be a stable branch, not the current change

`baseBranch` defaults to `main` and should name a committed branch or described commit, not the current working-copy commit (e.g. `jj log -r @` output): a jj `@` snapshots uncommitted launcher changes into an undescribed commit, so the worktree inherits that dirty tree, with a branch based on a commit jj refuses to push ("Won't push commit ... since it has no description"). Omit `baseBranch` for `main`, or name an explicit clean branch.

## Outputs

### `ctx.outputMaybe` is undefined until the task runs

Reading a downstream output before its task completes returns `undefined`, not a default: guard it so a not-yet-run task doesn't crash the render.

```tsx theme={"theme":{"light":"github-light","dark":"github-dark"}}
const analysis = ctx.outputMaybe(outputs.analysis, { nodeId: "analyze" });
return analysis ? <Task id="report" output={outputs.report} agent={writer}>...</Task> : null;
```

When a task only waits on one upstream output, `<Task deps={{ analyze: outputs.analysis }}>` with a `(deps) => ...` callback is more direct: it defers until the row exists and hands it straight to `children`, no guard variable needed.

### An output schema is a shared pool; do not write stray rows into one you filter

`ctx.outputs.<schema>` pools every row written for that schema, across every
node and loop iteration. Fan out, then filter on a discriminator field for
"the latest row for item X": any other task writing to that schema lands in
the same pool, so a setup or sentinel task reusing a filtered schema (say a
placeholder `validation` row from a `prepare` step) can be mistaken for real
per-item state. Give unrelated streams their own schema, and match on a
discriminator set on every row.

```tsx theme={"theme":{"light":"github-light","dark":"github-dark"}}
// prepare gets its OWN schema, not the validation schema the loop filters by deliverable
<Task id="prepare" output={outputs.prepare}>{() => ({ specFound: true })}</Task>
```

## Read next

* [How It Works](/how-it-works): the execution model these rules come from.
* [Recipes](/recipes): caching, hot reload, and VCS revert in context.
