moeru-ai/airi · error · Error

`send` received empty stdin input, Claude Code hooks events

Error message

`send` received empty 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:65) when stdin is piped (not a TTY) but the full contents read by readStdin() are empty or whitespace-only. This catches the case where a hook is wired up (so stdin.isTTY is false) but Claude Code — or a wrapper — piped nothing meaningful, which would otherwise produce a confusing JSON parse error or a silent no-op.

Source

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

      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> {
  cli.parse(argv, { run: false })

  if (cli.options.debug) {
    let namespace: string
    if (cli.options.debug === true) {

View on GitHub (pinned to 27111382b4)

Solutions

  1. Verify the producer of stdin actually emits a non-empty JSON payload — log `echo $PAYLOAD | wc -c` or the wrapper's stdout before piping.
  2. Confirm the hook event name (`hook_event_name`) is one Claude Code emits (e.g. UserPromptSubmit) and that the Claude Code version supports piping JSON for that event.
  3. If testing manually, ensure the piped string is a real JSON object, not just a newline: `printf '%s' '{"hook_event_name":"UserPromptSubmit","prompt":"hi"}' | airi-plugin-claude-code send`.
  4. Check any wrapper script in the Claude Code hook config for a missing `cat`/redirect or an early `exit` that discards the payload.

Example fix

# before: wrapper emits nothing
$ '' | airi-plugin-claude-code send
# -> `send` received empty stdin input...

# after: pipe a non-empty JSON hook event
$ printf '%s' '{"hook_event_name":"UserPromptSubmit","prompt":"hello"}' | airi-plugin-claude-code send
Defensive patterns

Strategy: validation

Validate before calling

// After reading stdin, fail fast with a precise message on empty/whitespace input.
const stdinInput = await readStdin()
if (stdinInput.trim().length === 0) {
  process.stderr.write('error: `send` received empty stdin. Confirm the hook pipes a JSON event.\n')
  exit(2)
}

Prevention

When it happens

Trigger: A Claude Code hook command is configured but the hook fires with an empty stdin (e.g. wrong hook type, or Claude Code version that does not emit JSON for that event); a wrapper script pipes the output of a command that produced no stdout; a user manually runs `echo '' | airi-plugin-claude-code send`; a piping bug drops the payload before the binary reads it.

Common situations: The hook is registered for an event that Claude Code does not actually populate with JSON; a shell wrapper redirects the wrong file/command into the binary; an upstream pipe stage failed silently and emitted no bytes; trailing newline-only input from a malformed producer.

Related errors


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