Skip to content
All writing

Typing into another window on Windows without hitting the wrong tab

Windows sends keystrokes to whichever window has the keyboard, and a terminal tab can't be picked from outside. Here is how Nook proves it has the right Claude Code tab before it types a reply, and when it refuses.

Nook lets you reply to a finished Claude Code session from a box in an island at the top of the screen. To deliver the reply it has to type it into the terminal where the session runs, and Windows will send keystrokes to whichever window has the keyboard. If that turns out to be a PowerShell tab, your reply runs as a command.

The version that didn't fit

The first version typed nothing. It started claude -p --resume <id> in the background, in the session's folder, with the reply on its standard input. No window touched, so nothing to get wrong.

It worked, and it was the wrong shape. The conversation carried on in a process nobody could see, and the terminal where you'd started the session showed nothing of it. So Nook now types the reply into that terminal, and the hard part is which one.

You can find the window but not the tab

Nook knows where a session runs. The relay that Claude Code starts reports the chain of processes above it, and that leads to a window. A Windows Terminal window holds many tabs, though, and nothing outside it can pick one. All the window tells you is its title, which is the title of the active tab.

Claude Code writes a title there: a status glyph, then the conversation's name. That's the proof. A window passes only if its title starts with a character that isn't ASCII, a letter or a digit, followed by this conversation's title. Either side may arrive cut short.

let first = window_title.trim_start()
    .chars().next();
let marked = first.is_some_and(|c| {
    !c.is_ascii() && !c.is_alphanumeric()
});
let (window, title) = (
    title_core(window_title),
    title_core(title),
);
if !marked
    || title.chars().count() < MIN_TITLE_CHARS
{
    return false;
}
window.starts_with(&title)
    || (window.chars().count() >= MIN_PREFIX_CHARS
        && title.starts_with(&window))

The glyph does the real work. A shell's own tab starts with a letter or a path, so Windows PowerShell fails. The tests pin the cases: ◐ Nook folders in personal repos passes, the same title without the glyph fails, and ◐ About Nook folders in personal repos fails too, because the conversation's title has to be where the window's begins.

Every check can say no

Before any key goes out, Nook runs seven checks in order. The first five refuse without typing anything.

Windows drops input aimed at a window that runs with more rights, so an administrator terminal can't be typed into. Nook says that instead of pretending it worked. Bringing a window forward takes time, and the tab can change while it does, so the title is proved a second time once the window is in front. Then Nook waits up to 700 ms for your own keys to be let go, so a held Shift or Enter doesn't land in the middle of the reply.

Typing is 32 keys at a time with a 12 ms pause, and before each chunk Nook asks whether the window is still in front. Click away halfway and it stops, doesn't press Enter, and tells you the text is partly there. Enter goes only after the last chunk, and only if the window is still in front.

The checks before Nook types a reply into a terminal

Seven checks in order, each one a gate. 1: find the session's window. 2: prove the window's title belongs to this conversation. 3: check the window is not running as administrator. 4: bring it forward, wait up to 600 milliseconds, and prove the title again. 5: wait up to 700 milliseconds for your own keys to be let go. 6: type 32 keys at a time, checking before each chunk that the window is still in front. 7: press Enter, only if the window is still in front. Checks 1 to 5 refuse without typing anything. At 6 and 7 a lost focus stops the typing and Enter is not pressed.

CHECKIF IT FAILSFind the windowplan_window(plan)1couldn't be foundProve the tabshows_conversation(title)2showing another tabCheck it isn't elevatedelevated_out_of_reach(hwnd)3runs as administratorBring it forward, prove againwait_foreground 600 ms4couldn't come forwardWait for your own keyswait_keys_released 700 ms5a key is still heldType 32 keys at a timeis_foreground before each chunk6focus left: interruptedPress Enteronly if still in front7focus left: no Enter
The checks before Nook types a reply into a terminal

Seven checks in order, stacked from top to bottom. 1: find the session's window. 2: prove the window's title belongs to this conversation. 3: check the window is not running as administrator. 4: bring it forward, wait up to 600 milliseconds, and prove the title again. 5: wait up to 700 milliseconds for your own keys to be let go. 6: type 32 keys at a time, checking before each chunk that the window is still in front. 7: press Enter, only if the window is still in front. Under each check is what happens when it fails: checks 1 to 5 refuse and type nothing; at 6 and 7 a lost focus stops the typing and Enter is not pressed.

Find the windowplan_window(plan)1couldn't be found · nothing typedProve the tabshows_conversation(title)2showing another tab · nothing typedCheck it isn't elevatedelevated_out_of_reach(hwnd)3runs as administrator · nothing typedBring it forward, prove againwait_foreground 600 ms4couldn't come forward · nothing typedWait for your own keyswait_keys_released 700 ms5a key is still held · nothing typedType 32 keys at a timeis_foreground before each chunk6focus left · interrupted, no EnterPress Enteronly if still in front7focus left · text typed, no Enter

Five checks refuse without typing a key. Once typing starts, losing focus stops it and Enter is never pressed.

Small things that bit

A new line in Claude Code is a backslash and then Enter, because Enter alone sends. So \n becomes those two keys, and a tab becomes four spaces. Codex doesn't read a backslash that way, so for Codex sessions newlines turn into spaces. Control characters such as Escape are refused outright, since a terminal would act on them.

Keys go in as Unicode input, a pair of units for anything that needs two, and the clipboard is never touched. The function returns an error only when nothing has been typed. After the first key it returns success, even for "interrupted", so a caller can trust that an error always means nothing happened.

Editors get a different route. The Claude Code extension in VS Code and Cursor can open a session by link with its prompt filled in, so Nook builds that link, percent-encodes the text, brings the editor forward and opens it. The reply waits in the prompt box for you to press Enter.

What it costs

Nook can't switch tabs for you. If another tab is showing, nothing is sent, and what you typed stays in the box with the reason. An elevated terminal gets the same answer. The reply box only appears while Nook can find the session's window, and it looks again every few seconds. A feature that sometimes says no is less pleasant than one that always types. When the wrong answer runs a command, I'll take the one that says no.

The takeaway is about any program that sends input to another one: prove what the window is, not just where it is, and prove it again after anything that waits. Windows calls like these have their own traps; the one that caught me in Go was a call reading a buffer the garbage collector had already freed. If you need tooling that sits beside a team's workflow without ever breaking it, that's the kind of automation work I take on.