---
title: Common Footguns
description: The handful of mistakes that bite people first, and the pattern that avoids each.
---

Smithers is durable by design, and most of its sharp edges trace back to one fact: a run is replayed from persisted state, not from memory. The mistakes below are the ones people hit first. Each links to the canonical reference for the full story.

## Resume and state

### Unstable task IDs break resume

The runtime keys completed work by task `id`. A changed id looks like a brand new task, and an id that disappears is dropped from the plan. Derive ids from data, never from a loop index or a timestamp.

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

It is the 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 when it starts. Resuming with different input is an error, not a silent override. If you need different input, start a new run.

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

A workflow source change is a different workflow. Resume validates the source hash of the original run, so editing the file and then resuming is blocked. Start a new run instead. To change a workflow that is still running, use hot reload (`up --hot`): 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 on every render, which here means every frame. Anything that must survive a crash belongs in a Task output read back through `ctx`, not in component state.

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

If a workflow writes verdict files such as `artifacts/.../verify.json` and later reads them to decide whether work is done, include `ctx.runId` in the evidence path or delete the evidence directory at run start. A prior run's `verify.json` must never be able to satisfy a fresh run's done-check.

## Caching

### Do not cache side-effecting tasks

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

## Side effects and retries

### Mark side-effecting tools and key them

Tasks retry, and a retried agent loop can call a tool again. A custom tool that writes to the world should declare `sideEffect: true` and pass `ctx.idempotencyKey` through to the downstream system so a retry is a no-op rather than a second charge. `ctx.idempotencyKey` is stable across retries and resumes for the same task iteration.

```ts
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 keeps a retry from double-charging. It does not make the
charge reviewable. An agent that both decides to send money and sends it cannot have
its decision inspected or rerun without risking a second charge. Have the deciding
task return a typed decision (`{ shouldPay, amount, reason }`), then put the actual
payout in its own downstream task behind an [`<Approval>`](/components/approval). The
decision is reversible and replayable; the act is 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` and `grep`, an implementer gets `write`, `edit`, and `bash`, and an agent with no `tools` cannot 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` constructed with just a `model` will prompt
for permission before it edits files. In an interactive session you click
through; in a detached run (`up -d`) there is no one to click, so the task stalls
until its heartbeat timeout. For autonomous runs construct agents with the bypass
flags:

```tsx
const opus = new ClaudeCodeAgent({
  model: "claude-opus-4-8",
  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,
});
```

The named pools in `.smithers/agents.ts` are intentionally bypass-free for
interactive use, so do not 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 process that is still running. It does not let you edit a workflow and
then `--resume` a previously suspended run (for example a detached run paused at
an approval gate): resume validates the source hash and rejects the changed file
with `RESUME_METADATA_MISMATCH`, with or without `--hot`. Changing task IDs or the
module graph always requires a fresh run. To iterate on a detached run that
suspends at gates, either keep a live `up --hot` process attached, or start a new
run and gate the already-finished phases off with an input flag so they skip.

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

A pinned `cwd` takes precedence over the per-task root, so the agent reads and
writes the launch directory and commits to the 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

These rewrite filesystem state, so treat them the way you would treat `git checkout` over uncommitted work.

- `revert` restores the workspace to a previous attempt's filesystem state and discards graph snapshots recorded after that attempt. It restores files only and lands them as a new change on top of 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 same source as the original run.

### `revert` requires jj

Smithers prefers `.jj` over `.git`. Pure Git repos run fine but cannot use `revert`, because there is no per-attempt change to restore. Install jj if you want attempt-level revert. See [VCS](/guides/vcs).

### Worktree runs auto-rebase on resume

On resume of a worktree run, Smithers rebases onto the base branch (default `main`) and continues 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 a described commit. Do not pass the current working-copy commit (for example `jj log -r @` output): a jj `@` snapshots the launcher's uncommitted changes into an undescribed commit, so the worktree inherits that dirty tree and its branch ends up based on a commit jj refuses to push ("Won't push commit ... since it has no description"). Omit `baseBranch` to use `main`, or pass an explicit clean branch name.

## Outputs

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

Reading a downstream output before its task has completed returns `undefined`, not a default. Guard it so a not-yet-run task does not crash the render.

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

When a task's only job is waiting on one upstream output, `<Task deps={{ analyze: outputs.analysis }}>` with a `(deps) => ...` children callback is the more direct form: 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>` is every row written for that schema by every node and
every loop iteration. When you fan out and later read "the latest row for item X"
by filtering on a discriminator field, any other task that writes a row to the
same schema lands in that pool too. A setup or sentinel task that reuses a
filtered schema (for example writing a placeholder `validation` row from a
`prepare` step) can be picked up as if it were real per-item state. Give
unrelated streams their own schema, and always match on a discriminator you set
on every row.

```tsx
// 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.
