Which Tab Was That? Tab Groups for Claude Code on macOS
Five Claude Code sessions, five tabs that all say zsh. Smart Terminal adds tab groups, Claude-aware tabs and alerts to a Terminal.app-style macOS terminal.
Running five Claude Code sessions in Terminal.app means five tabs that all say zsh, and no way to tell which one has been waiting on you for twenty minutes. I built Smart Terminal, a native macOS terminal that keeps everything I like about Terminal.app and adds Chrome-style tab groups, tabs that name themselves after the Claude session inside them, and a notification when one needs you. It went from an empty folder to a notarized public release in about two hours.
It was a normal afternoon: five Claude Code sessions open at once. A couple on client work, a couple on documents, and one on a WordPress plugin.
Every tab in the strip said zsh.
Somewhere in there, a session had finished twenty minutes earlier and was sitting on a question. I found it by clicking through the tabs one at a time.
Update, November 2026 (v0.15): agents can now drive a Chromium in the tab’s browser pane, Claude can hand you a command to run instead of asking you to type it, and sessions in tmux, on this Mac or over ssh, come back after a restart. What changed. Earlier updates: tabs in columns (v0.9) and a browser in each tab (v0.10).
Table of Contents
- Tabs alone do not fix it
- What Smart Terminal does
- Tabs that know Claude is in them
- Claude Code already writes it down
- Bringing your Terminal.app tabs over
- Built in an afternoon, with Claude Code
- What I got wrong
- Update: tabs in columns
- Update: a browser in each tab
- Update: agents in the browser, and sessions that come back
- Install it
Tabs alone do not fix it
Terminal.app is a good terminal. I have used it for years, with a profile I tuned once and never touched again. Its tabs, though, are just a row of shell names.
Browsers solved this a while ago. Chrome lets you gather tabs into a named, colored group and collapse it when you are not looking at it. Terminal.app has something called window groups, but those are saved layouts, not groups you work in.
I looked for a native macOS terminal with real tab groups and did not find one. The closest thing, TabTerm, gets there by running terminals inside Chrome to borrow Chrome's groups. That is clever, and it told me the gap was real.
Naming alone would not have solved it either. My problem was not only "which tab is the API one". It was "which of these Claude sessions is waiting for me".
What Smart Terminal does

- Rename any tab, and gather tabs into named groups in Chrome's nine colors. Click a group's chip to collapse it down to a count.
- Where you drop a tab decides its group: between two tabs of a group, it joins. Drag a chip to move the whole group, into another window, or out onto the desktop to make a new one.
- It still feels like Terminal.app. It reads your default Terminal.app profile, including font, colors and translucency, and the title bar shows the same live details: folder, program title, running command and window size.
- A tab running Claude Code takes the session's name and shows whether Claude is working, idle or waiting on you.
- When a Claude you cannot see finishes or hits a permission prompt, you get a macOS notification, and the Dock badge counts how many are waiting.
- Tabs remember the Claude session they were running and offer to resume it after a restart, with the same flags.
Your layout survives a restart: windows, groups, colors, names and the folder each tab was in.
Tabs that know Claude is in them
A tab running Claude Code names itself after the session. It uses your /rename if you set one, otherwise the short title Claude writes for every session, otherwise "Claude · folder". "Add Stripe webhook retries" is a lot more useful than zsh.
The status lives in the tab too. While Claude works, the tab shows the same half moon that Claude animates in its own terminal title, ◐ and ◑. When it goes idle, you get ✳. When it is blocked on a permission prompt, an orange speech bubble appears.
If you rename the tab yourself, your name wins, but the live status stays in front of it.
The notification only fires for tabs you cannot see: the app is in the background, or it is another tab. Click it and you land in that tab. That notification is the reason I started this.
Claude Code already writes it down
As with wims, most of the work was already done by Claude Code. It just was not visible.
Every running Claude process writes a small file to ~/.claude/sessions/, named after its process id:
{"pid":25965,"sessionId":"9b4e…","cwd":"/Users/me/code/api","status":"busy", …}
The status is busy, idle or waiting, and when it is waiting there is a waitingFor field that says why, for example permission prompt. The terminal title Claude sets does not tell you that, which is exactly the state I care about most.
The process id is the useful part. A terminal already knows which process group is in the foreground of each tab, and for a Claude tab, that is the Claude process. So detection is exact, without reading the screen or touching your shell config.
You can look at your own right now:
cat ~/.claude/sessions/*.json
The name comes from the session transcript, where Claude appends ai-title, custom-title, last-prompt and pr-link entries. Transcripts get big, so Smart Terminal reads backwards from the end until it finds a title, and after that only reads new bytes.
One more thing I found in Claude's source while building this. Claude Code can send its own notifications to the terminal, but it picks the method based on which terminal it detects. For any terminal it does not recognize, it sends nothing at all. Reading the session file gets around that without asking you to change a setting.
Bringing your Terminal.app tabs over
Switching terminals is painful when you have a dozen sessions open. So there is Shell → Import Tabs from Terminal.app…
It lists your open Terminal.app tabs, grouped by project folder, with a checkbox each:
- Claude sessions are handed over. Smart Terminal types
/exitinto the session in Terminal.app, waits for it to stop, then resumes it withclaude --resumeand the same flags. A session never runs in two places at once. - tmux sessions are re-attached here, and Terminal.app's client is detached. Nothing inside tmux restarts.
- Plain shells reopen in the same folder.
Sessions that are busy start unchecked, because handing one over stops it mid-turn. That default matters more than it sounds: the Claude session building this feature was itself in the list.
Built in an afternoon, with Claude Code
I started with an empty folder at 14:21. The notarized v0.5.0 was public on GitHub at 16:20.
In between, working in one Claude Code session: a tested core model for tabs and groups, the Swift app on top of SwiftTerm, everything above, and a release pipeline that signs, notarizes and packages a universal DMG. The result is about 4,500 lines of Swift and 52 tests, including a randomized one that throws 8,000 tab and group operations at two windows and checks nothing gets lost.
My job was the same as with the WordPress work I have written about: decide what it should be, test it with my hands, and say no when something felt wrong.
The most useful trick was giving the agent eyes. It could not take screenshots on my Mac, so it built a small debug channel into development builds. That channel can open tabs, type into them, rename and group them, and render the window to a PNG from inside the app. From then on it checked its own work visually before telling me it was done.
What I got wrong
A few things, and most were only caught by running the app.
Moving a tab to a new window crashed. The code read the layout while it was in the middle of changing it, and Swift's exclusivity checks stopped the app. The unit tests never crossed that path. The debug channel did, on its first try.
tab means two things in AppleScript. The import scan came back empty. Inside tell application "Terminal", the word tab refers to Terminal's tab object, not the tab character I was using to separate fields.
A bug that was not a bug. The first finished Claude turn after launch never showed as needing attention. Later ones did. After adding a log of every change, the answer was mundane: I had clicked the notification, which is supposed to clear it.
The process name is a version number. The tab titles briefly read my-project · 2.1.281, because Claude's binary lives in a versioned folder and that is what the system reports as its name. Detection goes through the session file instead.
Update: tabs in columns
A month in, with 27 tabs open across two groups, the strip had become the problem it was built to solve. Twenty-seven tabs do not fit in a row, so they shrink until every one of them is a few letters wide.
So the tabs moved into columns, the way Mail lays out mailboxes and messages:
- Groups on the left. All Sessions, then each group with its color and how many tabs it holds, then Ungrouped. An orange dot means something in that group is waiting on you.
- Sessions in the middle. One row per tab, with room to say more than a name: the folder, the git branch with how far it is ahead or behind, and the uncommitted diff as
+12 −4, the way a pull request shows it. If Claude is running there, its last prompt sits underneath, like the preview line of an email. - The terminal on the right, with a status bar under it. Claude's model, how full the context is and the session's tokens used to live in a sidebar pane. Now they are one line, and clicking it opens the details.
The git line is two read-only commands per folder every few seconds, git status and git diff --shortstat, run without taking git's lock so they never get in the way of a commit.
The strip is still there if you prefer it: View → Tabs in Columns.
One thing I got wrong on the way: the first version used SwiftUI's built-in selectable lists, and clicking a row only worked if you landed in the right spot. The rows also had their own drag and double-click handlers, and the two fought over every click. Plain rows that handle the click themselves fixed it.
Update: a browser in each tab

The next thing that kept pulling me out of the terminal was links. Claude finishes a PR and says “here it is”, I ask it to open the preview, and a browser window lands on top of everything, in whatever Space it was last in, detached from the session that asked for it.
So now each tab can have its own browser, in a pane beside its terminal:
- Ask Claude to open a page, and it opens there. Claude runs
open https://…like it always did; in Smart Terminal that lands in the tab's own pane instead of somewhere else. - ⌘-click a link in the terminal and it opens in the same pane.
- Switch tabs and the browser switches with you. Each session keeps its own page and history, so the PR stays next to the session that made it.
It is a real browser: back and forward, reload, an address bar that takes a URL, localhost:3000 or a search, and the full Web Inspector with the console, network and elements. It runs on WebKit, the engine Safari uses, so logins persist and the pane even shows up in Safari's Develop menu.
The interesting part was getting Claude's open to land there. Claude Code runs its commands in a fresh copy of your zsh, so the change had to live in the shell, without touching anyone's dotfiles. VS Code's terminal has the same problem and solves it with ZDOTDIR: point zsh at a folder of startup files that load yours unchanged, then add what you need. Smart Terminal does the same and adds one function, open, which sends a web URL to the app and passes everything else to macOS as before.
Update: agents in the browser, and sessions that come back
Since October the browser pane got a second job, and restarting the app stopped costing me my sessions. These are the changes up to v0.15, in November 2026.
- Agents get the pane too. Run devtools-fleet-mcp in a tab and the agent’s browser is that tab’s pane: a Chromium inside the app, with Google’s full DevTools toolset for navigation, clicks, screenshots, console, network and Lighthouse. I watch it work next to the session that asked for it, and can take over to log in. Each agent gets its own in-memory profile and sees only its own pages.
- Desktop or mobile. One button switches the pane to an iPhone layout and back. Agents switch the same way, with the tool they already have.
- A real Chrome when a site needs one. Google sign-in turns embedded browsers away, so an agent can move its browser out of the pane into Chrome, cookies and tabs included, and back again.
- Back and forward through tabs, as in Slack:
⌃⌘←and⌃⌘→. - Commands Claude asks you to run. Instead of telling me to type
! npm publish, Claude opens a tab beside the session with the command already typed. I press Return, and Claude gets the exit status and the output and carries on. - Sessions come back, tmux included. A Claude session resumes when I first look at its tab, so thirty of them don’t start at once. Tabs in tmux reattach, and tabs in ssh to tmux on another machine reconnect, with Claude still running over there. A small tmux pill marks the sessions that live in tmux.
One fix is worth telling. With an agent driving the pane, Smart Terminal itself sat above 100% CPU. Every message from the browser went through the main thread, and a full-page screenshot, several megabytes of it, was scanned again from the start each time another 64 KB arrived. Splitting and parsing each message once, off the main thread, took a run of 15 screenshots from timing out to about 20 seconds.
Install it
Download the DMG from the releases page, open it, and drag Smart Terminal to Applications. It is a signed and notarized universal build and needs macOS 26 (Tahoe) or later.
There are limits. macOS uses ⌃← and ⌃→ for switching Spaces by default, so moving a tab within its group has an ⌃⌥ alternative. There are no automatic updates yet.
Smart Terminal is MIT licensed and the code is on GitHub. It makes no network requests of its own; the browser loads only the pages you open.
If your terminal tabs all say zsh and one of them has been waiting on you, you now know which one.
Links: Smart Terminal on GitHub · wims · Claude Code docs · SwiftTerm
Tags: Claude Code, macOS, Terminal, Developer Tools