Build powerful CLI tools where Claude reasons in the cloud but executes commands on the user’s local machine using Possession Mode.
What You’ll Build
A CLI tool that:
- Accepts natural language commands
- Runs AI reasoning in Chucky’s cloud
- Executes file operations and shell commands locally
- Returns results to the user
Architecture
Quick Start
The simplest way is using the Chucky CLI directly:
# Install
npm install -g @chucky.cloud/cli
# Login and init
chucky login
chucky init
# Deploy your workspace
chucky deploy
# Run with possession mode
chucky prompt "Fix all TypeScript errors in src/" --allow-possession
How Possession Mode Works
When --allow-possession is enabled, Claude gets access to Host Tools that execute on the user’s machine:
| Tool | Description |
|---|
HostBash | Execute shell commands |
HostRead | Read files |
HostWrite | Write files |
HostEdit | Find/replace in files |
HostGlob | Find files by pattern |
HostGrep | Search file contents |
Claude reasons in the cloud (where your workspace context exists) but these tools execute locally.
Building a Custom CLI
Basic Implementation
// my-ai-cli/index.ts
import { ChuckyClient, createToken, createBudget } from '@chucky.cloud/sdk';
async function main() {
const prompt = process.argv.slice(2).join(' ');
if (!prompt) {
console.log('Usage: my-ai-cli "your request"');
process.exit(1);
}
// Create token (in production, get from your server)
const token = await createToken({
userId: 'cli-user',
projectId: process.env.CHUCKY_PROJECT_ID!,
secret: process.env.CHUCKY_HMAC_SECRET!,
budget: createBudget({ aiDollars: 1, computeHours: 1, window: 'hour' }),
});
const client = new ChuckyClient({ token });
const session = client.createSession({
model: 'claude-sonnet-4-5-20250929',
systemPrompt: {
type: 'preset',
preset: 'claude_code',
append: `You have access to Host tools that execute on the user's machine.
Working directory: ${process.cwd()}`
},
allowDangerouslySkipPermissions: true,
});
// Stream the response
await session.send(prompt);
for await (const msg of session.stream()) {
if (msg.type === 'assistant') {
const text = getAssistantText(msg);
if (text) process.stdout.write(text);
}
if (msg.type === 'result') {
console.log(`\n\nCost: $${msg.total_cost_usd.toFixed(4)}`);
}
}
session.close();
}
main().catch(console.error);
For tools to execute locally, you need to handle them in your CLI:
import { spawn } from 'child_process';
import { readFile, writeFile } from 'fs/promises';
import { glob } from 'glob';
// Host tool handlers
const hostTools = {
async HostBash({ command, timeout = 30000 }) {
return new Promise((resolve) => {
const proc = spawn('bash', ['-c', command], {
cwd: process.cwd(),
timeout,
});
let stdout = '';
let stderr = '';
proc.stdout.on('data', (data) => stdout += data);
proc.stderr.on('data', (data) => stderr += data);
proc.on('close', (code) => {
resolve({
content: [{ type: 'text', text: stdout || stderr }],
isError: code !== 0,
});
});
});
},
async HostRead({ path }) {
try {
const content = await readFile(path, 'utf-8');
return { content: [{ type: 'text', text: content }] };
} catch (err) {
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
}
},
async HostWrite({ path, content }) {
await writeFile(path, content);
return { content: [{ type: 'text', text: `Wrote ${content.length} bytes to ${path}` }] };
},
async HostGlob({ pattern }) {
const files = await glob(pattern, { cwd: process.cwd() });
return { content: [{ type: 'text', text: files.join('\n') }] };
},
};
// Handle tool calls
session.onToolCall(async (toolCall) => {
const handler = hostTools[toolCall.name];
if (handler) {
return handler(toolCall.input);
}
return { content: [{ type: 'text', text: `Unknown tool: ${toolCall.name}` }], isError: true };
});
Use Cases
Code Refactoring
my-ai-cli "Refactor all React class components to functional components with hooks"
Bug Fixing
my-ai-cli "Find and fix the bug causing the login form to submit twice"
Code Review
my-ai-cli "Review src/api/ for security vulnerabilities and fix any issues"
Test Generation
my-ai-cli "Generate unit tests for the UserService class"
Documentation
my-ai-cli "Add JSDoc comments to all exported functions in src/utils/"
Security Considerations
Possession Mode gives Claude the ability to execute arbitrary commands on the user’s machine. Only use with trusted prompts and consider these safeguards:
Recommended Safeguards
- Explicit opt-in: Require
--allow-possession flag
- Confirmation prompts: Ask before destructive operations
- Working directory isolation: Limit to project directory
- Command allowlist: Restrict which commands can run
- Dry-run mode: Show what would happen before executing
Example: Confirmation Flow
import { confirm } from '@inquirer/prompts';
session.onToolCall(async (toolCall) => {
if (toolCall.name === 'HostBash') {
const shouldRun = await confirm({
message: `Run command: ${toolCall.input.command}?`,
});
if (!shouldRun) {
return { content: [{ type: 'text', text: 'User declined' }], isError: true };
}
}
// Execute tool...
});
Applying Changes
When the AI makes file changes, they’re saved in a git bundle. Use the CLI to apply them:
# Auto-apply changes after completion
chucky prompt "Add error handling to api.ts" --allow-possession --apply
# Or manually
chucky fetch <session-id>
chucky diff <session-id>
chucky apply <session-id>
Best Practices
1. Clear System Prompts
Tell Claude what it has access to:
systemPrompt: {
type: 'preset',
preset: 'claude_code',
append: `
## Local Execution Context
- Working directory: ${process.cwd()}
- Available commands: npm, node, git, tsc
- Use HostBash for shell commands
- Use HostRead/HostWrite for files
`
}
2. Progress Feedback
Show users what’s happening:
for await (const msg of session.stream()) {
if (msg.type === 'tool_call') {
console.log(`\n[Tool] ${msg.name}`);
}
if (msg.type === 'tool_result') {
console.log(`[Result] ${msg.content[0]?.text?.slice(0, 100)}...`);
}
}
3. Error Recovery
Handle failures gracefully:
try {
await session.send(prompt);
for await (const msg of session.stream()) {
// ...
}
} catch (err) {
if (err instanceof BudgetExceededError) {
console.log('Budget limit reached. Upgrade your plan or wait for reset.');
} else if (err instanceof ConnectionError) {
console.log('Connection lost. Check your internet connection.');
} else {
console.error('Error:', err.message);
}
}
Next Steps