Safety and Human in the Loop

Safety is the reason Fisk AI exists. Two ideas carry it: a command runs with no shell and no credentials so it is bounded by construction, and a tagged command can require a human to approve it before it runs. Both default to the safe outcome.

Where it lives

internal/util: the confirm gate in confirm.go, the ask_human_* tools and terminal sanitization in builtin.go, the prompter contract in prompter.go and its line implementation in survey_prompter.go. Command execution safety is in fisk.go. The full-screen prompter is internal/tui/prompter.go.

A command is sandboxed by construction

When the loop runs a tool, Tool.Execute passes an argument vector to exec.CommandContext. No shell is involved, so model-supplied arguments can never be interpreted as shell syntax; t.Model.ArgsFromJSON is the trust boundary that bounds input to the command’s schema. The child gets no stdin, a ten-second wait delay after cancellation, an environment with credentials stripped, and its combined output capped.

model argsJSONargv execno shellraw outputstdout+stderrcap 64 KiBhead + tailchild envno secretsArgsFromJSON
A tool call becomes an argument vector, never a shell command. The child environment has its credentials removed and gains `LLMFORMAT=1`.

commandEnv removes five secret-bearing variables so a model-chosen command can never read the agent’s own credentials: ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, ANTHROPIC_IDENTITY_TOKEN, ANTHROPIC_WEBHOOK_SIGNING_KEY, and ANTHROPIC_CUSTOM_HEADERS (fisk.go:511). It appends LLMFORMAT=1 so a fisk application can render output suited to a model rather than a terminal. Output is capped at 64 KiB, keeping the head and tail with a truncation marker, so a chatty command cannot flood the model’s context.

Load-bearing decision

The built-in tools run in-process with the agent’s own, unstripped environment. Their handlers must never hand that environment to a subprocess. Only the exec path through commandEnv is sanitized (builtin.go:38).

Two ways to put a human in the loop

The two mechanisms are distinct and address different needs.

ai:confirm
An application author gates a specific command. The operator must approve it before it runs; the command itself is unchanged. Always active, no configuration flag.
human_in_the_loop
A configuration flag that gives the model three `ask_human_*` tools so it can ask the operator a question it chose to ask.

Reach for ai:confirm when a normal command should run only with the operator’s say-so. Reach for human_in_the_loop when the model should decide when to check in. The harness.confirm_tags key extends the gate to any tag an application already uses, for example impact:rw.

The confirm gate defaults to deny

When the model calls a gated command, the gate runs the checks in a fixed order, and every failure is a denial.

  1. Missing parameters first A structurally invalid call is rejected before the gate, so the operator is never asked to approve a broken command (`runner.go:608`).
  2. Session-allow short-circuit If the operator earlier chose "allow for the session" for this command, it runs without asking again.
  3. No terminal, no run Without an interactive terminal, or with a canceled context, the gate denies before any prompt is shown (`confirm.go:76`).
  4. Prompt the operator The sanitized full command line is shown. Any prompter error, an interrupt, an end-of-input, or an Escape, is a denial.

A denial is returned to the model as an authoritative, non-error result whose reason ends with a note not to retry, so the model reasons about the refusal rather than routing around a tool failure. An “allow for the session” answer is remembered by command name, regardless of arguments, and lasts only for that run.

The ask_human tools

When human_in_the_loop is enabled the model gets three tools: ask_human_confirm (yes or no), ask_human_select (choose one option), and ask_human_input (a free-text value). Each denies by default: a missing terminal, an interrupt, or an end-of-input yields a negative answer rather than a guess. Each renders on stderr so a piped final answer stays clean, and the model-supplied text is stripped of terminal control sequences first, before any truncation, so a cut can never leave a dangling escape.

Load-bearing decision

The confirm gate and the ask_human_* tools are agent-loop only and are never exposed over MCP or A2A, where there is no operator to answer. Over MCP a confirm-tagged command is requested through elicitation instead, which a client may auto-approve, so ai:deny is the only way to keep a command off a served surface entirely. See Interoperability.

Next

Continue to Sessions and Resume for how a run survives a suspend or a crash.