Skip to content

Bug: zprofile.pre.zsh / zshrc.pre.zsh hooks drop externally-injected shell input during the kiro-cli-term handoff #3876

Description

@abarbarioli

Checks

Operating system

macOS 26.5.2 (25F84)

Expected behaviour

When an external mechanism injects a command into a brand-new interactive/login zsh session before the shell finishes its startup files (e.g. Terminal.app's do script AppleEvent, a Finder double-click on a .command file, or a Finder alias to one), that command should still end up running once kiro-cli-term finishes wrapping the session. If replay truly can't be guaranteed, Kiro CLI should fail loudly (a warning/log) instead of silently discarding the input.

Actual behaviour

The command is silently lost. The user sees a normal shell prompt with no indication anything went wrong. This happens intermittently (a timing race), not on every launch.

Both of Kiro CLI's generated hooks contain the exact same vulnerable pattern:

  • kiro-cli init zsh pre --rcfile zprofile (sourced from .zprofile, which runs first, for login shells)
  • kiro-cli init zsh pre --rcfile zshrc (sourced from .zshrc, which runs after, for interactive shells)

This matters for reproduction: disabling only the .zshrc block is not enough to rule this out — .zprofile runs earlier and its hook alone is sufficient to reproduce the issue on its own, since Terminal.app's default new windows are both login and interactive shells. We initially disabled only the .zshrc block, still saw the failure, and only then noticed .zprofile carries an identical hook.

Root cause

The generated output of both kiro-cli init zsh pre --rcfile zprofile and kiro-cli init zsh pre --rcfile zshrc contains the identical block below (only the surrounding rcfile differs — the vulnerable logic is copy-identical in both):

if   [[ -t 1 ]] \
  && [[ -z "${PROCESS_LAUNCHED_BY_Q:-}" ]] \
  && command -v kiro-cli-term 1>/dev/null 2>&1 \
  && [[ ("${SHOULD_QTERM_LAUNCH}" -eq 0) || (("${SHOULD_QTERM_LAUNCH}" -eq 2) && (-z "${Q_TERM:-}" || (-z "${Q_TERM_TMUX:-}" && -n "${TMUX:-}"))) ]]
then
  ...
  if [[ $- == i ]]; then
    Q_TERM_NAME="$(basename "${Q_SHELL}") (kiro-cli-term)"
    ...
    Q_EXECUTION_STRING="${BASH_EXECUTION_STRING:=$ZSH_EXECUTION_STRING}"
    # Get initial text.
    INITIAL_TEXT=""
    if [[ -z "${BASH:-}" || "${BASH_VERSINFO[0]}" -gt "3" ]]; then
      while read -rt 0; do
        if [[ -n "${BASH:-}" ]]; then
          read -r
        fi
        INITIAL_TEXT="${INITIAL_TEXT}${REPLY}\n"
      done
    fi
    Q_EXECUTION_STRING="${Q_EXECUTION_STRING}" Q_START_TEXT="$(printf "%b" "${INITIAL_TEXT}")" Q_SHELL="${Q_SHELL}" Q_IS_LOGIN_SHELL="${Q_IS_LOGIN_SHELL}" exec -a "${Q_TERM_NAME}" "${Q_TERM_PATH}"
  fi
fi

while read -rt 0; do ... done is a non-blocking drain loop: it checks whether stdin already has buffered, unread bytes and, if so, consumes all of them into INITIAL_TEXT, which is then handed to kiro-cli-term via the Q_START_TEXT environment variable so it can be replayed once kiro-cli-term finishes taking over the pty (exec -a ... "${Q_TERM_PATH}").

This creates a race with any external mechanism that "types" a command into a brand-new interactive shell by writing it straight into the pty before the shell is fully initialized:

  1. Terminal.app creates the window/pty and starts the login shell.
  2. The external trigger (Terminal's do script, or Finder resolving a .command file/alias and handing it to Terminal) writes the target command string into the pty's input queue. This can happen before zsh has finished sourcing .zprofile/.zshrc — there is no synchronization between "Terminal.app considers the command sent" and "the shell has reached the point where it's ready to read commands".
  3. zsh reaches the Kiro CLI pre-block (in .zprofile, or later in .zshrc if it somehow wasn't already hijacked by then). read -rt 0 finds the already-buffered command text sitting in the input queue and slurps it up as INITIAL_TEXT.
  4. The current process execs into kiro-cli-term, expecting it to replay Q_START_TEXT into the freshly wrapped session.
  5. If that replay fails, is delayed, or is otherwise dropped (we did not dig into kiro-cli-term's internals — it's a closed binary from our side), the command is gone. The user is left looking at a completely ordinary shell prompt inside the kiro-cli-term-wrapped session, with zero indication that a command was ever sent.

Why this is hard to notice / hard to attribute

  • It's intermittent — purely a timing race between (a) when the external trigger's text lands in the pty buffer and (b) when the shell reaches this point in .zshrc. Sometimes the text arrives after the drain loop has already run (or before kiro-cli-term has fully handed off), in which case everything works.
  • It reproduces across every mechanism we tried that works by injecting text into a new shell: Finder double-click on a .command file, a Finder alias to that file, a plain symlink to that file, and a compiled AppleScript .app using tell application "Terminal" to do script "..." (even with an added delay 0.5 between activate and do script — the delay doesn't help because the race is between the shell's own startup and the pty write, not between activate and do script).
  • It does not reproduce when a command is typed manually into an already-running interactive shell, because by then .zshrc (and this hook) already finished running long before the user starts typing — there's no startup race left to lose to.

Impact

This isn't specific to one script — it affects any automation that opens a new Terminal.app window and expects a command to run in it: build scripts, "run in Terminal" launchers, Automator/AppleScript apps, Finder .command shortcuts, CI helper scripts that shell out to osascript, etc. Anyone using Kiro CLI's terminal wrapping on macOS with Terminal.app is exposed to this.

Steps to reproduce

  1. Ensure Kiro CLI's zsh integration is active (default ~/.zshrc blocks, as above).
  2. Create any simple test script, e.g. ~/test.command:
#!/bin/zsh
echo "IT RAN: $(date)" >> /tmp/test_marker.log
read -p "press enter"

chmod +x ~/test.command.
3. Repeatedly open a brand-new Terminal window that runs this script via any of:

  • Double-clicking test.command in Finder,
  • osascript -e 'tell application "Terminal" to do script "~/test.command"',
  1. Check /tmp/test_marker.log after each attempt. On a meaningful fraction of attempts (we saw roughly ~30%, though it likely depends on machine load / how "cold" Terminal.app's launch is), no line is appended — the window opens, shows the command text, and returns to a bare prompt, but the script never ran.
  2. To confirm the Kiro CLI hook is the cause rather than Terminal.app/Finder itself: comment out the Kiro CLI pre/post block lines in both ~/.zprofile and ~/.zshrc (disabling only one is not a valid test — see Actual behaviour above), fully quit and relaunch Terminal.app (not just close/reopen a window — window restoration can otherwise carry over an already-hijacked session), and repeat steps 3–4.

Status at time of writing: we disabled .zshrc's block first, still reproduced the failure, then discovered .zprofile has an identical block and disabled that too. Confirmation that disabling both fully resolves it is in progress with the user — this report will be updated once that's confirmed either way.

How we ruled out other causes first

Before landing on this, we tested and ruled out, in order:

  • macOS Gatekeeper / quarantine / provenance re-scanning after editing the target script (no matching entries in log show around failure times; not it).
  • Finder alias vs. symlink vs. plain file staleness after editing the target script (inode changes do make aliases stale, but switching to a symlink — immune to inode changes — did not fix the issue, ruling this out as the cause here, though it's a real, separate footgun worth knowing about).
  • zsh interactive shell startup being slow due to the Zinit plugin manager (zsh-autosuggestions, fast-syntax-highlighting) — measured zsh -ilc 'exit' at a consistent ~0.15s across five runs, not slow/variable enough to plausibly explain multi-second-scale intermittent failures.
  • Adding a delay 0.5 between activate and do script in an AppleScript-based launcher — did not help, which is expected once you know the race is between the shell's own startup and Terminal's pty write, not between activate and do script.

Suggested directions for a fix

  • Don't discard INITIAL_TEXT if the handoff to kiro-cli-term can't guarantee replay — or make the replay synchronous/blocking with a confirmation, instead of "fire and forget" via an env var.
  • Alternatively, don't drain stdin with read -rt 0 at all during this handshake; let kiro-cli-term itself inherit the still-buffered stdin naturally (since it's about to exec into the same pty anyway, any unread bytes should still be sitting in the kernel's tty input queue for the next reader) instead of trying to capture-and-replay them in userspace.
  • At minimum, surface a warning/log when INITIAL_TEXT is non-empty but the replay into kiro-cli-term didn't happen (so this fails loudly instead of silently swallowing user/automation input).

Environment

<This will be visible to anyone. Do not include personal or sensitive information>

[q-details]
version = "2.5.0"
hash = "9595f8ef1891859828df3785e926266512d6762b"
date = "2026-05-28T23:17:47.748855Z (60d ago)"
variant = "full"

[system-info]
os = "macOS 26.5.2 (25F84)"
chip = "Apple M5 Pro"
total-cores = 15
memory = "48.00 GB"

[environment]
cwd = "/Users/USER/Pessoal/Fontes/macOS Scripts"
cli-path = "/Users/USER/Pessoal/Fontes/macOS Scripts"
os = "Mac"
shell-path = "/bin/zsh"
shell-version = "5.9"
terminal = "macOS"
install-method = "unknown"

[env-vars]
PATH = "/Users/USER/.local/share/zinit/polaris/bin:/Users/USER/.antigravity-ide/antigravity-ide/bin:/Users/USER/.local/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/pkg/env/global/bin:/usr/local/share/dotnet:~/.dotnet/tools"
SHELL = "/bin/zsh"
TERM = "xterm-256color"
__CFBundleIdentifier = "com.apple.Terminal"

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions