Permission flows
Wire an approval UI without diverging per runtime.
The user-facing pattern is the same across runtimes: the consumer app subscribes to permission events, shows a dialog, posts back the verdict. SNA hides Claude's PreToolUse hook vs Codex's JSON-RPC approval behind one event.
Server side
Pick a permission mode when starting the agent:
await client.agent.start(sessionId, {
provider: "claude-code",
permissionMode: "default", // prompts; alternatives: "acceptEdits" | "bypassPermissions"
});Subscribe + respond
await client.agent.subscribePermissions();
client.agent.onPermissionRequest(async ({ session, request }) => {
const approved = await showApprovalDialog(request);
await client.agent.respondPermission(session, approved);
});The same callback fires for Claude PreToolUse hooks and Codex
approval/request notifications.
Safe-tool allowlist
Tools that match the configured allowlist bypass the prompt entirely.
The default list includes Read, Glob, Grep, LS and other
read-only operations. To extend:
await client.agent.start(sessionId, {
provider: "claude-code",
allowedTools: ["Read", "Glob", "Grep", "Bash(ls:*)", "Bash(git status:*)"],
});Patterns use Claude Code's tool-pattern syntax. The same string list is normalized per-runtime, so the consumer config doesn't need to fork by runtime.
Renderer pattern (Electron / React)
The bundled <SnaChatUI> ships a permission dialog out of the box.
Wire <SnaProvider> around your app and the prompt flows
automatically. For custom UI:
function PermissionGuard({ children }: React.PropsWithChildren) {
const [pending, setPending] = useState(null);
const client = getSnaClient();
useEffect(() => {
client.agent.subscribePermissions().catch(() => {});
return client.agent.onPermissionRequest(setPending);
}, [client]);
return (
<>
{children}
{pending && (
<Dialog
request={pending.request}
onApprove={() => client.agent.respondPermission(pending.session, true)}
onDeny={() => client.agent.respondPermission(pending.session, false)}
/>
)}
</>
);
}