Claude Code v2.1.141: the terminalSequence field solves desktop notifications in headless CI
The new terminalSequence hook output field lets Claude Code emit OSC escape sequences through its own terminal write path, fixing no-TTY environments.
Claude Code v2.1.141 adds a single new field to hook JSON output: terminalSequence. It sounds minor. For anyone running Claude Code in CI, in headless mode, or as a background agent, it closes a real gap that opened up two releases earlier.
A quick recap of what hooks are
Hooks are shell commands, HTTP endpoints, or LLM prompts you wire into Claude Code’s lifecycle. They fire automatically at specific points, like before a tool runs, after a file is written, or when a session ends. The key property is that they are guaranteed to run. The model doesn’t decide whether to call them; Claude Code does. That makes them the right place for things like auto-formatting, blocking risky operations, injecting context, or signalling that a long-running task has finished.
The problem that appeared in v2.1.139
Two releases before terminalSequence arrived, Claude Code changed how it runs command hooks on macOS and Linux. Hook processes were moved into their own session, without a controlling terminal. That’s a reasonable security improvement: the hook process and any child processes can no longer open /dev/tty, which limits what a malicious or misconfigured hook can do to your terminal state.
The side effect is that any hook trying to write OSC escape sequences directly to the terminal now fails silently. That rules out OSC 9 desktop notifications, OSC 0 window title updates, and the plain ASCII BEL character (\u0007) used for terminal bells. All three are useful signals when you want to know that an agent running in the background has finished or is waiting for input.
What terminalSequence does
The fix is a delegation model. Instead of writing escape sequences itself, the hook returns them to Claude Code inside a JSON field:
{ "terminalSequence": "\u0007" }
Claude Code receives that output and emits the sequence through its own terminal write path. Because Claude Code itself has a controlling terminal, the sequence gets through. The hook never needs one.
This approach also works inside tmux and GNU screen, where direct TTY writes often cause ordering problems. And it works on Windows, where /dev/tty doesn’t exist at all.
What sequences are allowed
The field accepts a string containing one or more allowlisted escape sequences. The supported types are OSC 9, OSC 0, and OSC 777, which cover desktop notifications (in terminals that support them, like Kitty, iTerm2, and Windows Terminal), window title updates, and Growl-style notifications respectively. The plain BEL character is also accepted for a cross-platform bell.
Sequences outside the allowlist are rejected and the field is ignored entirely. That means CSI cursor and colour sequences, OSC 52 clipboard writes, OSC 8 hyperlinks, and OSC 1337 are all blocked. The allowlist exists to prevent hooks from using this pathway to manipulate the terminal in ways that could affect the user’s session.
What this means for you
If you run Claude Code in CI or headless mode, this is the supported way to get a terminal bell or window title update when a session completes. Standard tools like osascript and notify-send still work for native desktop notifications because those don’t require a TTY. But anything that needs to write to the terminal now needs to go through terminalSequence.
If you use background agents, wiring a terminalSequence bell into your Stop hook means you’ll get an audible signal when Claude finishes a turn, without polling or switching windows. Combine it with an osascript call for the macOS notification and a notify-send call for Linux, and you have a single hook that covers all three.
A minimal cross-platform bell hook looks like this:
#!/bin/bash
# Build the BEL escape sequence and return it via terminalSequence
printf '{"terminalSequence": "\u0007"}'
The printf approach keeps the control byte out of the shell command line. If you’re constructing a more complex payload with a message, use jq -n --arg to handle quoting correctly so newlines and backslashes in the notification text don’t break the JSON.
One thing to watch: the hook’s stdout must contain only the JSON object. If your shell profile prints anything on startup, it will interfere with JSON parsing and the field will be silently ignored.
If you’re building CI/CD pipelines with human-in-the-loop approval, the terminalSequence bell pairs well with PreToolUse hooks that exit with code 2. That exit code pauses a headless session and waits for --resume, so you can gate specific tool calls on manual sign-off. The bell tells the operator something needs attention without requiring them to watch the terminal continuously.
A note on the Stop hook loop cap
v2.1.143, released shortly after, adds a safety cap worth knowing about. If a Stop hook returns "decision": "block" eight times in a row for the same turn, Claude Code ends the session with a warning rather than looping indefinitely. The threshold is configurable via CLAUDE_CODE_STOP_HOOK_BLOCK_CAP. If you’re using a Stop hook for notifications, make sure it doesn’t accidentally return a blocking decision, or you’ll hit this cap on longer tasks.
The version requirement
terminalSequence requires Claude Code v2.1.141 or later. If you’re on an older version, the field will be ignored without error, so upgrading is straightforward.
The same release also added CLAUDE_CODE_PLUGIN_PREFER_HTTPS for environments without a GitHub SSH key, ANTHROPIC_WORKSPACE_ID for workload identity federation, and claude agents --cwd <path> to scope the session list to a working directory. Useful additions, but terminalSequence is the one that unblocks a real workflow problem for headless and background agent setups.