DEV Community

Cover image for Headless Claude Code asks, exits, then resumes on the answer
Panth Patel
Panth Patel

Posted on Originally published at panth.vardayinitech.in

Headless Claude Code asks, exits, then resumes on the answer

When my coding agent needs a decision from me, it asks in the ticket and then stops. The process exits. My answer starts it again with claude -p --resume on the same session id, and the values I picked arrive as its next prompt.

The constraint: by default the orchestrator runs one agent at a time, and a run is capped at 240 minutes. A session sitting idle until I read my notifications would spend both.

I'm Panth, and I lead the software team at Oizom. This is from ticket-tracker, my open-source board where headless Claude Code sessions work tickets next to people. The orchestrator is the workspaces/ folder: one process per board, one Claude Code session per ticket.

The loop

The README draws it like this:

To do  ──orch claims──▶ In progress ──agent reports "review"──▶ Review ──owner──▶ Done
                            │  ▲
           agent asks a     │  │ the answer (form or a comment) resumes
           question ────────▼  │ the same session
                          waiting
Enter fullscreen mode Exit fullscreen mode

"waiting" is not a stage on the board. The ticket stays In progress. It is a phase the orchestrator keeps in its own state file, next to the session id.

What the agent is told

Every agent gets the same brief. Step 3 of it:

If a real decision is the owner's to make (scope, design, anything destructive or outward-facing), call ask_question with blocking: true and real options. Then stop working and finish your turn with status: "waiting" and the question_id. Do not poll for the answer. The orch resumes this same session with the answer when it arrives.

ask_question puts a form card in the ticket thread: a title, optional context, and 1 to 10 fields (single choice, multiple choice, text, number, yes/no, date). A blocking question puts a "Waiting for you" badge on the ticket card and notifies whoever it is addressed to. When I submit it, the card locks and shows my answer.

The agent never decides on its own that it is finished. Its last message is structured output, enforced with --json-schema:

const OUTCOME_SCHEMA = {
  type: 'object',
  properties: {
    status: { type: 'string', enum: ['review', 'waiting', 'blocked'] },
    summary: { type: 'string' },
    question_id: { type: 'string' },
  },
  required: ['status', 'summary'],
};
Enter fullscreen mode Exit fullscreen mode

Three ways to end a turn, and each one means something different to the orchestrator.

status What the orch does next
review Moves the ticket to Review and stops.
waiting Heartbeat "Waiting for an answer", phase waiting, keeps the question_id. Nothing runs until a person answers or comments.
blocked Carries on by itself, up to 3 times (autoResume), then posts on the ticket that it has stopped.

The split between the last two matters. waiting means a person is needed. blocked on a long ticket almost always means the turn ran out of room, so the orchestrator resumes it without asking anyone, a bounded number of times.

Why the session doesn't wait

Each run is one claude -p process. When it exits, the orchestrator posts a receipt with what the turn cost and marks the slot free, so the next ticket in To do can start. With maxAgents: 1, a session waiting for me would hold the only slot. Every other ticket on the board would wait behind my inbox.

There is a second reason in the brief. The session is headless, so when a turn ends the process exits and takes its background tasks with it. A turn either finishes its work or ends cleanly. There is no half-alive process in between for me to find later.

And nothing polls. The agent is told not to. The orchestrator doesn't sit on a timer either: it wakes on the board's revision, and the answer reaches it as an event.

How the answer gets back in

When I submit the form, the tracker puts a question_answered event in the agent's inbox. The orchestrator drains its inbox, sees the event for a ticket in waiting, and resumes the session with a prompt built from my answer:

answered: (t, q) => `The owner answered your question "${q.title}" on ${t.key}.

Values: ${JSON.stringify(q.answer?.values ?? q.values ?? {})}
Comment: …

Carry on with the work.`,
Enter fullscreen mode Exit fullscreen mode

The resume itself is one flag. If the session file for that id still exists, the orchestrator passes --resume <id>; if not, it starts a fresh session and resets its cost to zero.

...(resume ? ['--resume', sessionId] : ['--session-id', sessionId]),
Enter fullscreen mode Exit fullscreen mode

The agent picks up with its whole context: the files it read, its plan, the question it asked. It doesn't re-read the repo to work out where it was.

A few edges are handled on the way:

  • A comment instead of the form. If I reply in the thread rather than filling in the card, that also resumes the session, with my message in the prompt.
  • No slot free. If another ticket is running, the ticket's status line says "Got your reply · queued until an agent is free", so an answered question never looks ignored.
  • Cancelled or expired. The prompt says so, and tells the agent to read the thread, then go ahead with the most conservative option or ask again.
  • A missed event. Every fourth loop, a ticket still waiting has its question fetched directly, in case the event was lost.
  • The answer is progress. It resets the ticket's automatic-continuation count to zero, and so does any comment from me.

Cost stays honest across resumes

Claude Code reports total_cost_usd cumulatively across --resume. In a session that asked two questions, summing the reported totals would count the first turn's cost three times. So the receipt for a turn is the difference from the session's last reported total, and the session's running total lives in the orchestrator's state.

What it costs me

A question costs no compute while I'm away: there is no process to keep alive. The price is latency: the ticket sits until I answer, and nothing else picks it up. I'm fine with that. The questions an agent asks are the decisions I said were mine.

How does your agent setup handle a question it can't answer itself: does the session wait, or does it end the turn and come back?

Top comments (0)