Permission requests and closing
Answer a permission request, send another message, and close the session
Run one session from start to finish. The agent asks for permission to write a file, the script allows it, the next message continues in the same working tree, and at the end you close the session. Use the werft client from Install the SDK. The key also needs sessions:approve and sessions:control.
Create a session that writes a file
This script writes hello.txt with Bash tee. The claude-coding-local profile does not ask for file writes and edits inside the session workspace or for local git. It asks for permission before any other command that changes files, such as tee, which is not on that list. A redirect into the workspace (echo alpha > hello.txt) does not ask, so this example does not use it.
import {
closeResult,
type PendingRequestHandlers,
waitForReceipt,
waitForTurn,
} from "@werft-ai/sdk";
import { werft } from "./werft.ts";
const signal = AbortSignal.timeout(5 * 60_000);
const COMMAND = "echo alpha | tee hello.txt";
const created = await werft.sessions.create({
profile_id: "claude-coding-local",
repository_id: "sample-app",
message: `GATE-SPEC ${JSON.stringify({
id: "first",
steps: [
{
tool: "Bash",
input: { command: COMMAND, description: "write hello.txt" },
},
],
final: "wrote hello.txt",
})}`,
});
const sessionId = created.session_id;Answer permission requests
When the session asks for permission and stops, its status becomes needs_input. Pass handlers to waitForTurn, and the permission requests and questions that come up while it waits for the turn go to them.
// Allow the exact command this script expects, deny everything else.
const approvals: PendingRequestHandlers = {
permission(request) {
console.log("asked:", request.tool, request.input);
const command = (request.input as { command?: unknown } | null)?.command;
if (request.tool === "Bash" && command === COMMAND)
return { action: "allow" };
return { action: "deny", reason: `This script only allows: ${COMMAND}` };
},
};
const first = await waitForTurn(werft, sessionId, created.turn_id, {
signal,
onRequest: approvals,
onAnswer: (outcome) => console.log(`answered: ${outcome.outcome}`),
});
console.log(`turn ${first.turn_id}: ${first.status}`);asked: Bash { command: 'echo alpha | tee hello.txt', description: 'write hello.txt' }
answered: accepted
turn 1: completed- If a handler returns
undefined, or there is no handler for that kind, the request stays open for a person. - A denial is
{ action: "deny", reason }. A request nobody answers ends as denied after 30 minutes. The details are in Waiting for approval and resuming. - A workspace key (personal or agent) cannot approve requests from a session it started. How to use a separate key for approvals is in Client settings and errors.
Send another message
A message to the same session becomes the next turn. The worker keeps the same working tree and the same Claude Code conversation. Read-only commands do not ask for permission.
const sent = await werft.sessions.messages.send(sessionId, {
message:
'GATE-SPEC {"id":"second","steps":[{"tool":"Bash","input":{"command":"cat hello.txt","description":"read hello.txt"}}],"final":"read it"}',
});
const second = await waitForTurn(werft, sessionId, sent.turn_id, { signal });
console.log(`turn ${second.turn_id}: ${second.status}`);turn 2: completedClose
Close a session once its work is done. It runs the current turn to the end, leaves a checkpoint and becomes closed. A control request takes the session's current revision as expected_revision. The SDK does not read it for you, and it does not resend after a REVISION_CONFLICT.
const { revision } = await werft.sessions.get(sessionId);
const accepted = await werft.sessions.close(sessionId, {
expected_revision: revision,
reason: "first session done",
});
const receipt = await waitForReceipt(werft, accepted.receipt_id, { signal });
console.log(`close: ${receipt.status}`, closeResult(receipt));
const session = await werft.sessions.get(sessionId);
console.log(session.status, session.admission_state);close: succeeded {
checkpoint_revision: 1,
cancelled_turn_count: 0,
resulting_admission_state: 'closed'
}
stopped closedA closed session takes no more messages (409 SESSION_CLOSED).
Doing it with the HTTP API
Read permission requests with GET /v1/sessions/{id}/pending-requests and answer with POST /v1/sessions/{id}/answers. Messages are POST /v1/sessions/{id}/messages, and closing is POST /v1/sessions/{id}/close. Every POST needs an Idempotency-Key header.
Next steps
- Client settings and errors: options, error types, retries
- Start: fields and errors of the session creation request
- Create: define an agent and bind it to sessions