Voor de assistent in onze hosting bouwden we een eigen chat-interface op de Claude Agent SDK (TypeScript). De documentatie legt de basis goed uit, maar een paar dingen ontdek je pas als je het echt bouwt. Dit zijn onze lessen, getest met SDK-versie 0.3.270.
Toestemming vragen met canUseTool
Met canUseTool beslis je per tool-aanroep wat er gebeurt. Wij tonen een goedkeurkaart en wachten tot de gebruiker klikt:
const options = {
permissionMode: "default",
canUseTool: async (name, input, { signal, toolUseID }) => {
if (["Read", "Glob", "Grep"].includes(name)) return { behavior: "allow", updatedInput: input };
const answer = await askUser(name, input, signal); // jouw UI
return answer.allow
? { behavior: "allow", updatedInput: input }
: { behavior: "deny", message: "De gebruiker heeft deze stap afgewezen." };
},
};
Luister naar signal: bij een interrupt wordt de vraag afgebroken, en je UI moet de kaart dan sluiten.
AskUserQuestion: het antwoord gaat via updatedInput
Claude stelt meerkeuzevragen via de tool AskUserQuestion. Die komt óók langs canUseTool. Behandel je hem als gewone goedkeuring ("toestaan"), dan krijgt Claude geen antwoord en stelt de vraag daarna opnieuw in platte tekst.
Het antwoord hoort in updatedInput.answers, met de vraagtekst als sleutel:
if (name === "AskUserQuestion") {
const answers = await showChoiceButtons(input.questions); // { "Wat wordt de prijs?": "€ 1.150" }
return { behavior: "allow", updatedInput: { ...input, answers } };
}
Bij meerdere keuzes (multiSelect) scheid je de labels met komma's.
Auto-modus: je ziet de wijzigingen niet meer vanzelf
permissionMode: "auto" laat een classifier bepalen of een stap veilig is. Het werkt goed, maar goedgekeurde stappen komen niet langs canUseTool. Een bestandswijziging is dan onzichtbaar in je interface.
Onze oplossing: een PostToolUse-hook die elke uitgevoerde wijziging alsnog meldt, en een PreToolUse-hook om de oude inhoud te bewaren voor een diff:
hooks: {
PostToolUse: [{ matcher: "Edit|Write|MultiEdit", hooks: [async (h) => {
if (!shownAsCard.has(h.tool_use_id)) emit({ type: "auto-edit", tool: h.tool_name, input: h.tool_input });
return {};
}] }],
}
Moet iets altijd een vraag blijven, ook in auto-modus? Geef in een PreToolUse-hook permissionDecision: "ask" terug. Wij doen dat voor het terugzetten van backups.
strictMcpConfig blokkeert ook plugin-MCP
Draait de SDK onder een gebruiker die op claude.ai connectors heeft (Gmail, Slack), dan laadt de sessie die mee. Met strictMcpConfig: true voorkom je dat. Maar: dan laden ook de MCP-servers uit je plugins niet meer.
Geef ze daarom expliciet door. Lees de .mcp.json van je plugins en zet die in mcpServers:
mcpServers: JSON.parse(fs.readFileSync(`${pluginDir}/.mcp.json`, "utf8")).mcpServers,
strictMcpConfig: true,
De tools heten daarna mcp__<server>__<tool>, handig om leesacties automatisch toe te staan.
Live meekijken met includePartialMessages
Zonder streaming ziet de gebruiker 30 seconden "bezig…" terwijl Claude een groot bestand schrijft. Met includePartialMessages: true krijg je stream_event-berichten:
text_deltavoor tekst die binnenkomt,thinking_deltavoor denkstappen,input_json_deltavoor tool-invoer. Hieruit haal je vroeg hetfile_pathen het aantal bytes, voor een regel als "Schrijft index.html · 14 KB".
Let op dubbele tekst: het volledige assistant-bericht kan binnenkomen vóór de laatste delta's. Leeg je delta-buffer zodra het complete tekstblok er is.
Een sessie hervatten
Bewaar het session_id uit het system/init-bericht. Met resume: sessionId gaat een nieuw proces verder met dezelfde context. Claude weet dan nog wat er besproken is. De weergave van het oude gesprek moet je zelf opslaan: de SDK geeft je bij hervatten geen geschiedenis terug.
Houd de runner netjes
Draai je de SDK in een apart proces met stdin/stdout naar je server, laat het proces dan stoppen als stdin sluit. Anders blijven er na een herstart van je server "wezen" draaien. Bij ons liep dat op tot vier processen en ruim 1 GB geheugen voordat we het zagen.
readline.createInterface({ input: process.stdin }).on("close", () => process.exit(0));