Handle approvals and user input
Permission requests are part of the runtime contract. Your app can allow, deny, or escalate tool execution without guessing what the agent is doing.
Permission modes
The current SDKs all expose a mode that controls whether tools run immediately or stop for review.
- `interactive`: emit permission requests and wait for your app to respond.
- `unrestricted`: allow all tool execution without asking.
- `restricted`: block risky operations automatically.
- `external`: hand the decision to your own callback or policy layer where supported.
TypeScript
TypeScript
import { AutohandSDK } from '@autohandai/agent-sdk';
async function main() {
const sdk = new AutohandSDK({
cwd: '.',
permissionMode: 'interactive',
});
await sdk.start();
for await (const event of sdk.streamPrompt({
message: 'Run the test suite and summarize failures.'
})) {
if (event.type === 'permission_request') {
console.log('Permission needed for', event.tool);
await sdk.permissionResponse({
requestId: event.requestId,
allowed: event.tool !== 'run_command',
});
continue;
}
if (event.type === 'message_update') {
process.stdout.write(event.delta);
}
}
await sdk.stop();
}
main();
Python
import asyncio
from autohand_sdk import AutohandSDK
async def main():
async with AutohandSDK(cwd='.', permission_mode='interactive') as sdk:
async for event in sdk.stream_prompt('Run the test suite and summarize failures.'):
if event['type'] == 'permission_request':
await sdk.respond_to_permission(
event['request_id'],
decision='allow',
allowed=event.get('tool') != 'run_command',
)
continue
if event['type'] == 'message_update':
print(event.get('delta', ''), end='')
asyncio.run(main())
Go
package main
import (
"context"
"fmt"
"log"
autohand "github.com/autohandai/agent-sdk-go"
)
func main() {
ctx := context.Background()
sdk := autohand.NewSDK(&autohand.Config{
CWD: ".",
PermissionMode: autohand.PermissionInteractive,
})
if err := sdk.Start(ctx); err != nil {
log.Fatal(err)
}
defer sdk.Close()
events, err := sdk.StreamPrompt(ctx, &autohand.PromptParams{
Message: "Run the test suite and summarize failures.",
})
if err != nil {
log.Fatal(err)
}
for event := range events {
switch e := event.(type) {
case autohand.PermissionRequestEvent:
if err := sdk.PermissionResponse(ctx, e.RequestID, true, autohand.ScopeOnce); err != nil {
log.Fatal(err)
}
case autohand.MessageUpdateEvent:
fmt.Print(e.Delta)
}
}
}
Swift
import AgentSDK
import Foundation
let hookManager = HookManager()
let permissionManager = PermissionManager(
hookManager: hookManager,
mode: .ask
)
Runner.setPermissionManager(permissionManager)
let provider = OpenAIProvider(apiKey: "sk-...")
let agent = Agent(
name: "Reviewer",
instructions: "Review commands before anything destructive runs.",
tools: [.readFile, .bash],
model: ModelID("gpt-4o"),
provider: provider
)
let stream = Runner.runStream(
agent: agent,
prompt: "Run the test suite and summarize failures."
)
for try await event in stream {
if event.type == .content, let data = event.data {
print(data, terminator: "")
}
}
Java
import ai.autohand.sdk.sdk.AutohandSDK;
import ai.autohand.sdk.types.DecisionScope;
import ai.autohand.sdk.types.Events;
import ai.autohand.sdk.types.PromptParams;
import ai.autohand.sdk.types.SDKConfig;
AutohandSDK sdk = new AutohandSDK(new SDKConfig(
".",
null,
false,
300000
));
sdk.start();
sdk.streamPrompt(new PromptParams("Run the test suite and summarize failures."), event -> {
if (event instanceof Events.PermissionRequestEvent pre) {
sdk.allowPermission(pre.requestId(), DecisionScope.ONCE);
} else if (event instanceof Events.MessageUpdateEvent mue) {
System.out.print(mue.delta());
}
});
sdk.stop();Approval patterns
Most apps end up using one of these shapes:
- Auto-allow read-only tools and ask on shell or write operations.
- Allow everything inside a short-lived sandboxed worker.
- Deny dangerous tools and return a manual follow-up task to a human.
- Remember a decision for a single session while a review flow stays open.
Selective approval
TypeScript
const sdk = new AutohandSDK({
cwd: '.',
permissionMode: 'interactive',
});
for await (const event of sdk.streamPrompt({ message: 'Inspect git status and run tests.' })) {
if (event.type !== 'permission_request') {
continue;
}
const autoAllow = event.tool === 'read_file' || event.tool === 'git_status';
await sdk.permissionResponse({
requestId: event.requestId,
allowed: autoAllow,
remember: autoAllow,
});
}
Python
async for event in sdk.stream_prompt('Inspect git status and run tests.'):
if event['type'] != 'permission_request':
continue
auto_allow = event.get('tool') in ('read_file', 'git_status')
await sdk.respond_to_permission(
event['request_id'],
decision='allow' if auto_allow else 'deny',
allowed=auto_allow,
)
Go
for event := range events {
req, ok := event.(autohand.PermissionRequestEvent)
if !ok {
continue
}
autoAllow := req.Tool == "read_file" || req.Tool == "git_status"
if err := sdk.PermissionResponse(ctx, req.RequestID, autoAllow, autohand.ScopeOnce); err != nil {
log.Fatal(err)
}
}Best practices
- Make your approval UI show the tool name, description, and working directory.
- Do not silently auto-allow shell commands unless the agent is inside an explicit sandbox boundary.
- Keep the permission mode close to the job. Read-only review jobs and write-enabled refactor jobs should not share the same default.
- Log every allow or deny decision if the run matters for CI, auditing, or customer support.