moeru-ai/airi · error · Error

`send` doesn't work without stdin input, Claude Code hooks e

Error message

`send` doesn't work without stdin input, Claude Code hooks events are expected to be piped to this command.

What it means

Thrown by the `send` CLI action (cli.ts:60) when process.stdin.isTTY is true, i.e. the command was run in an interactive terminal rather than having a Claude Code hook event piped to its stdin. The send command exists solely to receive the JSON hook payload that Claude Code pipes to configured hook commands, so an interactive invocation has no data to read and is rejected up front before readStdin() blocks forever.

Source

Thrown at plugins/airi-plugin-claude-code/src/cli.ts:60

  .action(async (_, flags: Options) => {
    if (flags?.quiet) {
      logger = logger.withLogLevel(-1 as LogLevel)
    }
    else {
      logger = logger.withLogLevelString(flags?.logLevel ?? LogLevelString.Log)
    }

    async function readStdin(): Promise<string> {
      const chunks: string[] = []
      for await (const chunk of stdin) {
        chunks.push((chunk as Buffer).toString('utf-8'))
      }

      return chunks.join('')
    }

    if (stdin.isTTY) {
      throw new Error('`send` doesn\'t work without stdin input, Claude Code hooks events are expected to be piped to this command.')
    }

    const stdinInput = await readStdin()
    if (!stdinInput.trim()) {
      throw new Error('`send` received empty stdin input, Claude Code hooks events are expected to be piped to this command.')
    }

    const hookEvent = JSON.parse(stdinInput) as HookInput

    if (hookEvent.hook_event_name === 'UserPromptSubmit') {
      const channelServer = new Client({ name: 'proj-airi:plugin-claude-code', autoConnect: false })
      await channelServer.connect()

      channelServer.send({ type: 'input:text', data: { text: hookEvent.prompt } })
    }
  })

export async function runCLI(): Promise<void> {

View on GitHub (pinned to 27111382b4)

Solutions

  1. Pipe a Claude Code hook event JSON into the command: `echo '{"hook_event_name":"UserPromptSubmit","prompt":"hi"}' | airi-plugin-claude-code send`.
  2. In real use, do not invoke `send` by hand — register it as a Claude Code hook command so Claude Code pipes the hook payload to stdin automatically.
  3. If you need to test the binary in a TTY context, feed it via stdin redirection (`< payload.json`) or `printf '...' |` so stdin.isTTY is false.
  4. Double-check the Claude Code hook configuration references this binary as the command, not a parent shell that swallows stdin.

Example fix

# before: interactive run, stdin.isTTY === true
$ airi-plugin-claude-code send
# -> `send` doesn't work without stdin input...

# after: pipe the hook payload
$ echo '{"hook_event_name":"UserPromptSubmit","prompt":"hello"}' | airi-plugin-claude-code send
# or: airi-plugin-claude-code send < hook-event.json
Defensive patterns

Strategy: validation

Validate before calling

// Guard the CLI action: only attempt readStdin when stdin is actually piped.
if (stdin.isTTY) {
  process.stderr.write('error: `send` requires piped hook JSON. Usage: ... | airi-plugin-claude-code send\n')
  exit(2)
}

Prevention

When it happens

Trigger: Running `airi-plugin-claude-code send` (or the package's bin) directly from a shell prompt; invoking the binary in a CI step that allocates a TTY but does not pipe a payload; calling the command from a script that forgot to echo/pipe the hook JSON into it.

Common situations: A developer runs the binary manually to smoke-test the hook wiring; the hook entry in Claude Code settings points to the wrong command so Claude Code never pipes anything; a wrapper script launches the binary in a TTY-allocating shell.

Related errors


AI-assisted analysis of moeru-ai/airi@27111382b4 (2026-08-12). Data as JSON: /api/errors/8fd65dda4fe9db57. Report an issue: GitHub.