Crux
CookbookBasics

CLI tool approvals

Use the same approval middleware in a terminal app.

Approval is just message state. A CLI can run once, print the requested action, ask the user, append a tool-approval-response, and run again.

approval-cli.ts
import readline from 'node:readline/promises'
import { stdin as input, stdout as output } from 'node:process'
import { prompt } from '@crux/core'
import { approvalMiddleware } from '@crux/core/tool-middleware'
import { toolApprovalResponse } from '@crux/core/adapter/tool'
import { generate, tool } from '@crux/ai'
import { openai } from '@ai-sdk/openai'
import { z } from 'zod'

const shell = tool({
  description: 'Run a safe shell command.',
  inputSchema: z.object({ command: z.string() }),
  execute: async ({ command }) => runAllowedCommand(command),
})

const assistant = prompt({
  id: 'cli-agent',
  input: z.object({ task: z.string() }),
  system: 'Help with local maintenance. Ask for approval before running shell.',
  prompt: ({ input }) => input.task,
  tools: { shell },
  toolMiddleware: [
    approvalMiddleware({
      id: 'shell-approval',
      match: ['shell'],
      onApproved: ({ input }) => audit.write({ action: 'approved', input }),
      onDenied: ({ reason }) => audit.write({ action: 'denied', reason }),
    }),
  ],
})

const first = await generate(assistant, {
  model: openai('gpt-4o'),
  input: { task: 'Clean temporary files.' },
})

const request = first.response.messages
  .flatMap((message) => (Array.isArray(message.content) ? message.content : []))
  .find((part) => part.type === 'tool-approval-request')

if (!request) {
  console.log(first.text)
  process.exit(0)
}

const rl = readline.createInterface({ input, output })
const answer = await rl.question(`Approve ${request.toolCall.toolName}? [y/N] `)
rl.close()

const approved = answer.toLowerCase() === 'y'

const final = await generate(assistant, {
  model: openai('gpt-4o'),
  input: { task: 'Clean temporary files.' },
  messages: [
    ...first.response.messages,
    {
      role: 'tool',
      content: [
        toolApprovalResponse({
          approvalId: request.approvalId,
          approved,
          ...(!approved ? { reason: 'Denied in CLI' } : {}),
        }),
      ],
    },
  ],
})

console.log(final.text)

This pattern is useful for local agents, admin tools, and CI scripts where approval can be a terminal prompt instead of a modal.

On this page

No Headings