Skip to main content
The run context includes an agent boolean that indicates whether the command is being invoked by an agent or a human.

Overview

cli.command('deploy', {
  args: z.object({ env: z.enum(['staging', 'production']) }),
  run(c) {
    if (!c.agent) console.log(`Deploying to ${c.args.env}...`)
    return { url: `https://${c.args.env}.example.com` }
  },
})

How It Works

c.agent is true when:
  • stdout is not a TTY — output is piped, redirected, or consumed programmatically
  • --json or --format is used — explicit format flags indicate agent consumption
  • --mcp is used — running as an MCP stdio server
Otherwise, c.agent is false (human mode).

Use Cases

Progress Messages

Show progress to humans without polluting agent output:
cli.command('build', {
  async run(c) {
    if (!c.agent) console.log('Building...')
    const result = await build()
    if (!c.agent) console.log('Done!')
    return result
  },
})
# Human mode
$ my-cli build
# Building...
# Done!
# → files: 42
# → duration: 3.2s

# Agent mode (no progress messages)
$ my-cli build --json
# → {"ok":true,"data":{"files":42,"duration":3.2}}

Interactive Prompts

Skip prompts when invoked by agents:
import prompts from 'prompts'

cli.command('deploy', {
  args: z.object({ env: z.enum(['staging', 'production']).optional() }),
  async run(c) {
    let env = c.args.env
    if (!env && !c.agent) {
      const response = await prompts({
        type: 'select',
        name: 'env',
        message: 'Choose environment',
        choices: [
          { title: 'Staging', value: 'staging' },
          { title: 'Production', value: 'production' },
        ],
      })
      env = response.env
    }
    if (!env) return c.error({ code: 'MISSING_ENV', message: 'env is required' })
    return { deployed: true, env }
  },
})

Colored Output

Use colors for humans, plain text for agents:
import chalk from 'chalk'

cli.command('status', {
  run(c) {
    const status = 'online'
    if (!c.agent) console.log(chalk.green(`Status: ${status}`))
    return { status }
  },
})

Logging

Log to stderr in human mode, stay silent in agent mode:
cli.command('sync', {
  run(c) {
    if (!c.agent) console.error('[sync] Starting...')
    const result = doSync()
    if (!c.agent) console.error(`[sync] Synced ${result.files} files`)
    return result
  },
})

Spinners and Animations

Only show spinners in human mode:
import ora from 'ora'

cli.command('install', {
  async run(c) {
    const spinner = c.agent ? null : ora('Installing...').start()
    try {
      const result = await install()
      spinner?.succeed('Installed!')
      return result
    } catch (error) {
      spinner?.fail('Install failed')
      throw error
    }
  },
})

Detection Heuristics

incur uses the same heuristics as standard CLI tools:
const agent = process.stdout.isTTY !== true
This is true when:
  • Output is piped: my-cli deploy | jq
  • Output is redirected: my-cli deploy > output.txt
  • Running in an agent tool call (MCP, skill invocation)
This is false when:
  • Running in a terminal emulator (iTerm, Terminal.app, etc.)
  • Running in an SSH session with a TTY

Middleware Access

Middleware also has access to c.agent:
import { middleware } from 'incur'

const logger = middleware((c, next) => {
  if (!c.agent) console.error(`[${c.command}] Starting...`)
  const result = await next()
  if (!c.agent) console.error(`[${c.command}] Done`)
  return result
})

cli.use(logger)

Best Practices

Do

  • Use console.log() or console.error() for human-facing messages
  • Use return for structured data that goes to both agents and humans
  • Check c.agent before prompting for input
  • Check c.agent before showing progress indicators

Don’t

  • Don’t change the return value based on c.agent — return the same data structure in both modes
  • Don’t use c.agent to hide errors — errors should always be shown
  • Don’t use c.agent for authentication — it’s a UI hint, not a security boundary

Build docs developers (and LLMs) love