# Interactive Clarification

Forced interactive clarification is a **Guide + Sensor** pair (see
[the harness model](/harness/overview#the-harness-model-guides-and-sensors)),
not one of the [enforced gates](/harness/overview#the-enforced-set). The
**Guide** is a canonical callout embedded in every asset that asks a user a
question; the **Sensor**, `forge clarify-gate`, reads two persisted marker
scopes and warns — but by design can never refuse a run — when one is left
unanswered.

::: warning Not one of the enforced gates
`forge clarify-gate` deliberately never returns Claude Code's blocking
`PreToolUse` exit code, is wired only to lifecycle-end events (`Stop`,
`session.idle`) that fire *after* the agent's turn has already ended, and is
not consumed by `forge review validate` at any delivery gate. It surfaces a
warning; it does not stop anything. Read this whole page before assuming it
gives the same guarantee as `AskUserQuestion` or `question` themselves.
:::

## The Guide: one canonical callout

Every asset that asks the user a discrete or open-ended question — the SDD
gap commands, `sdd-propose`, `clarify.md` itself, and the orchestrator
workflow prompt — carries the same byte-identical callout:

```markdown
> **Interactive input (required):** If your harness provides an interactive
> question/selection tool (Claude Code's `AskUserQuestion`, OpenCode's
> `question`), you MUST use it for every question in this step — one tool
> call per decision point — instead of rendering the question as plain chat
> text. For open-ended questions, still use the tool: offer the 2–4 most
> plausible concrete answers as selectable options and let the user supply
> their own wording through the tool's free-text ("Other") option. Do not
> downgrade an open-ended question to chat prose, do not answer on the
> user's behalf, and do not continue past the question without a real user
> turn. Only if your harness has no interactive tool at all: ask in plain
> text, then STOP and wait for a new user message before any further action.
```

That single paragraph carries every rule a phase skill or command needs:

- **Discrete choices** go through the harness's native tool —
  `AskUserQuestion` on Claude Code, `question` on OpenCode — one tool call
  per decision point, never rendered as plain chat text.
- **Open-ended questions** still use the tool. The asset offers 2–4
  plausible concrete answers as selectable options and lets the user type
  their own wording through the tool's **"Other"** free-text option, instead
  of downgrading to chat prose.
- **Plain-text fallback is the last resort**, permitted only when the active
  harness genuinely has no interactive-tool equivalent — and even then the
  skill must STOP and wait for a real new user message before continuing.

It is installed byte-identical in every one of these assets, so a single
golden-byte-comparison test — not a review — is what keeps them from
drifting apart:

`commands/{infra,brand,constitution,analyze,refine,brainstorm,drift}.md`,
`commands/clarify.md` (both the multiple-choice and the short-answer path),
`skills/sdd-propose/SKILL.md` (Step 0), and
`claude/sdd-orchestrator-workflow.md` (widened from covering only the
orchestrator's own cached decisions to any clarifying question raised by any
phase skill or command it delegates to). OpenCode's own orchestrator asset
carries the equivalent widened guidance too.

## The Sensor: `forge clarify-gate`

`forge clarify-gate` reads two persisted marker scopes and performs **zero
model judgment** — it never decides whether a question is ambiguous, it only
reports what a skill already wrote to disk.

```text
forge clarify-gate [--cwd <repo>] [--session-id <id>] [--change <name>] [--format text|json]
```

| Scope | File | Written by |
| --- | --- | --- |
| per-change | `.forge/changes/<change>/clarify.yaml` | change-bound commands — propose, spec, design, tasks, apply, clarify |
| per-session | `.forge/clarify/sessions/<session-id>.yaml` | change-less commands — `/forge.brand`, `/forge.constitution`, `/forge.infra`, `/forge.brainstorm` |

`--change` explicit wins; omitted, the gate falls back to the single active
change the same way `forge hook` does. `--session-id` only activates the
session scope when it sanitizes cleanly — empty, missing, or anything that
looks like a path (`/`, `\`, `..`, a null byte) silently degrades to
change-scope-only rather than erroring or ever being joined into a
filesystem path.

```text
$ forge clarify-gate --cwd . --change checkout
clarify-gate: clean (no unresolved clarifying questions)
# exit 0

$ forge clarify-gate --cwd . --change checkout
CLARIFY GATE — unresolved clarifying question(s) found. This is a warning, not a hard block; the session continues.
  [change:checkout] 1 unresolved:
    - q1: Which auth model?
A human answers via the interactive tool, or waives with: forge clarify waive --id <qid> [--change <c>|--session <id>]
# exit 1
```

That non-zero exit is the entire warning. It folds every unresolved
question's scope, id, and text into the returned error so a host's uniform
`Error: …` stderr print carries the detail — `clarify-gate` needs no
dedicated stderr writer of its own. `--format json` emits the same
`clean`/`scopes[]` structure for a host that wants to parse it instead of
reading text.

### The marker lifecycle

A skill writes a marker the moment it decides a question is needed, **before**
it calls the interactive tool:

```sh
forge clarify open --id q1 --question "Which auth model?" --asked-in sdd-propose --change checkout
```

Three commands can move a question out of `status: open`:

```sh
forge clarify answer --id q1 --tool AskUserQuestion --answer-digest <12hex> --change checkout
forge clarify waive  --id q1 [--reason "…"]                                --change checkout
forge clarify list --change checkout   # print only what is still unresolved
```

## The trust gap: `answer` is best-effort

::: danger `answer` is agent-runnable, and that is an honest limit, not a bug
`forge clarify answer` requires a recognised tool name
(`AskUserQuestion`/`question`) and a 12-hex answer digest — the same
fingerprint discipline `secscan` uses — but forge has **no way to verify**
the agent didn't invent both. An agent that wants to self-clear a marker and
answer on the user's behalf can technically do so. `clarify-gate` prints
`answered` entries as an informational tally specifically so a human can
spot-check them; it treats them as resolved either way.
:::

`forge clarify waive` is the only path forge actually trusts, because it
never accepts caller-supplied identity: `waived_by` always comes from
`identity.Resolve(root)` — the resolved git/environment identity, never
text an agent passes on the command line. This mirrors `secscan`'s "a human
waives, never the agent" discipline exactly (see
[Secret & PII Scan](/harness/secscan#the-waiver-workflow)). If you need the
strong guarantee that a human, not an agent, actually cleared a question,
`waive` is the only command that gives it to you — `answer` never does.

## Installer wiring

### Claude Code — the `Stop` hook

`forge install` (and `forge init`) appends an idempotent `Stop` hook entry
to `~/.claude/settings.json` (or the project's `.claude/settings.json`),
alongside every other lifecycle hook Forge manages:

```json
{ "hooks": { "Stop": [
  { "hooks": [ { "type": "command", "command": "forge clarify-gate --cwd \"${CLAUDE_PROJECT_DIR:-$PWD}\"" } ] }
] } }
```

There is deliberately no `|| true` on this line — same convention as the
verify circuit-breaker's `Stop` hook in
[Connect a live agent](/harness/live-agent-hooks): the gate is *meant* to
return non-zero so Claude Code surfaces it, not so it can silently fail
open. The insertion is append-if-absent and idempotent: re-running
`forge install` never duplicates the entry and never disturbs a pre-existing
`Stop` array you already had.

Verify it installed:

```sh
grep -A2 '"Stop"' ~/.claude/settings.json
```

### OpenCode — the `session.idle` plugin

OpenCode has no `Stop`-hook equivalent, so its wiring uses the structurally
equivalent `session.idle` event — the point where the agent's turn actually
ends — instead of forcing a Claude-Code-style shape onto a different host.
`forge install` writes the plugin to:

```text
~/.config/opencode/plugins/clarify-gate.ts
```

The plugin `execFile`s the `forge` binary with a 30-second timeout, passing
`event.properties.sessionID` as `--session-id`, and **never throws** — a
failed or unresolved gate surfaces as a toast
(`input.client.tui.showToast({ variant: "warning" })`), never as a plugin
crash. It is purely observational: `session.idle` cannot refuse the stop, it
can only report after the fact.

Verify it installed:

```sh
ls ~/.config/opencode/plugins/ | grep clarify-gate.ts
```

## The v1 non-goal: ten harnesses are not covered

Forge supports twelve agent harnesses. This release wires the clarify-gate
backstop into exactly **two**: Claude Code (`Stop` hook) and OpenCode
(`session.idle` plugin). The remaining ten —
**cursor, codex, gemini, kiro, kimi, qwen, hermes, antigravity, windsurf,
and generic** — get the canonical callout Guide (the asset text itself asks
the agent to use whatever interactive tool exists, or fall back to plain
text and stop), but **no Sensor at all**. Nothing in those ten harnesses
reads or warns on an unresolved `clarify.yaml`/`sessions/<id>.yaml` marker
today.

This gap is intentional, not an oversight, and it is not marked
`(Deferred: <target>)` in the spec because no concrete future change name
exists yet to serve as that target. If you rely on one of the other ten
harnesses, the only enforcement you have is the Guide text itself — the same
honest limit `AskUserQuestion`/`question` parity would not have given you
anyway, since those tools are Claude Code- and OpenCode-specific to begin
with.

## Where it fits

`clarify-gate` bounds a different failure mode than the
[four enforced controls](/harness/overview#the-enforced-set): not runaway
iterations, unbounded spend, a dangerous action, or unreviewed delivery, but
an agent quietly self-answering a product decision it should have routed
through the user. It shares their shape — a persisted marker, a
deterministic read, a non-zero exit — but not their guarantee: it is a
Sensor that warns, wired only to lifecycle-end events on two of twelve
supported harnesses, never a gate that blocks.

## See also

- [The Enforcement Doctrine](/harness/overview) — where the four enforced
  controls sit, and why `clarify-gate` is not a fifth one.
- [Connect a live agent](/harness/live-agent-hooks) — the `Stop` hook and
  `session.idle` wiring in full.
- [Secret & PII Scan](/harness/secscan) — the sibling gate `clarify waive`
  borrows its human-only waiver discipline from.
- [The forge CLI](/reference/cli) — the full `clarify-gate` and `clarify`
  flag reference.
