A CLI for managing Chrome tabs, windows and groups.
Install • Extension • Quick start • Commands • Harness • Contributing
A tab manager for Chrome that lives in your terminal. Every verb takes
--json, and tabbrew init writes AI coding agents a cheat sheet, so you
can hand tab, window and group management to the agent you already work
with.
You need two things: the tabbrew binary (Install) and the
TabBrew extension
from the Chrome Web Store. The CLI talks to the extension over 127.0.0.1
and only touches the tabs you name.
Each command goes over 127.0.0.1 to the TabBrew extension, which runs it
against the Chrome you already have open and reports back. Nothing changes
unless a command names it.
| Command | What it does |
|---|---|
tabbrew session start / stop |
Start or stop the local session that links this terminal to Chrome |
tabbrew tabs list |
Every open tab across every window, one row each |
tabbrew tabs create |
Open a URL in a new background tab: at the end of a window, next to a tab, or inside a group |
tabbrew tabs focus |
Select a tab and raise its window |
tabbrew tabs move |
Put tabs next to another tab, across windows if needed |
tabbrew tabs group |
Gather tabs into a new or existing group, with title, colour and collapsed state |
tabbrew tabs ungroup |
Take tabs out of their group |
tabbrew tabs discard |
Unload tabs from memory, keeping them on the tab strip |
tabbrew tabs reload |
Reload tabs in place, optionally bypassing the cache |
tabbrew tabs close |
Close tabs |
tabbrew windows list |
One row per open window, with tab and group counts |
tabbrew windows create |
Open a new window, empty or with URLs |
tabbrew groups list |
One row per tab group, with colour and collapsed state |
tabbrew groups collapse / uncollapse |
Fold or expand groups |
tabbrew groups close |
Close groups while Chrome keeps them among its saved groups |
tabbrew init |
Write a cheat sheet into CLAUDE.md / AGENTS.md so an AI agent can drive the browser |
tabbrew update |
Replace the installed binary with the latest release |
tabbrew uninstall |
Stop the session, remove ~/.tabbrew and the binary it installed |
Every method below installs only the tabbrew binary. It also needs the
TabBrew extension
from the Chrome Web Store; see Set up the extension.
brew install colevels/tap/tabbrewThe formula in colevels/homebrew-tap
is regenerated by the release workflow from the same binaries and checksums as
the other methods below. Upgrade with brew upgrade tabbrew.
curl -fsSL https://raw.githubusercontent.com/colevels/tabbrew-cli/main/install.sh | shPicks the binary for your OS and CPU from the latest release, verifies its
SHA-256 against checksums.txt, and installs tabbrew to ~/.local/bin
(override with TABBREW_INSTALL_DIR). Read install.sh first
if you prefer.
Download tabbrew-<os>-<arch> from the releases
page, chmod +x it
and put it on your PATH. Every asset is built in GitHub Actions and carries a
SLSA build-provenance attestation; verify one with the GitHub
CLI:
gh attestation verify tabbrew-darwin-arm64 --repo colevels/tabbrew-cliRequires Bun 1.1 or newer on the machine: the package ships TypeScript that Bun runs directly.
npm install -g tabbrew-cli # or: bun install -g tabbrew-cli
bunx tabbrew-cli --help # one-off, no installgit clone https://github.com/colevels/tabbrew-cli && cd tabbrew-cli
bun install
bun link # puts `tabbrew` on your PATH, pointing at this checkoutInstall TabBrew
from the Chrome Web Store. That is all: tabbrew session start opens its
connection page by id, and if that page never answers it prints the Store
link to install from.
To drive another build instead, the harness or an unpacked checkout of the
product, load it through chrome://extensions → Developer mode → Load
unpacked and set TABBREW_EXTENSION_ID to the id Chrome shows there, or
record it once for the project with tabbrew init --extension <that id>
(--extension harness for the harness, --extension store to go back), which
keeps it in .tabbrew.json next to the agent docs. The
harness is tabbrew-extension.zip on the release that matches
tabbrew --version;
its page tells you when the two drift apart.
tabbrew session start # starts the local session and opens the connection page in Chrome
tabbrew tabs list # every open tab, across every window
tabbrew init # write the cheat sheet for AI agents into CLAUDE.mdThe session exits on its own after 10 idle minutes; tabbrew session start
again brings it back. Nothing works while no extension page is open, and the
CLI never changes a tab that you did not name.
| Page shows | Meaning |
|---|---|
| No session | Nothing answered on either port. Run tabbrew session start. |
| Connected | Address, pid, CLI version and uptime of the session, and the last command the page served. It stays alive while the page is open. |
Installed the binary (install script or download):
tabbrew update --check # current vs latest, changes nothing (--json for scripts)
tabbrew update # download the latest release, verify its checksum, swap the binary in placeupdate reads the newest version from the releases/latest redirect (no API
token, no rate limit), downloads the asset for your OS and CPU, checks it
against checksums.txt and atomically replaces the running executable. It is
a no-op when you are current. TABBREW_UPDATE_REPOSITORY,
TABBREW_UPDATE_LATEST_URL and TABBREW_UPDATE_DOWNLOAD_BASE_URL point it at
a fork or mirror.
Installed with Homebrew: brew upgrade tabbrew. From npm: npm install -g tabbrew-cli@latest. From source: git pull && bun run build. tabbrew update
refuses to touch any of these.
On macOS 27, a binary from 0.9.12 or earlier dies with a bare killed before
it can run update: those releases carry an invalid code signature. Run the
install script again; it replaces the binary without starting the old one.
After any update, run tabbrew session stop then tabbrew session start so
the new version serves (tabbrew update reminds you when a session is
running), and re-run tabbrew init in repos that carry the cheat sheet.
Every other command also checks for a newer release in the background, at
most once every 24 hours (state kept in ~/.tabbrew/state.yml), and prints a
one-line notice on stderr when one is found. Set TABBREW_NO_UPDATE_CHECK=1
to disable it.
tabbrew uninstall --dry-run # show what would go, change nothing
tabbrew uninstall # stop the session, remove ~/.tabbrew and the binary
brew uninstall tabbrew # if installed with Homebrew
npm uninstall -g tabbrew-cli # if installed from npm; from source: bun unlinkuninstall removes the binary only when it is the one install.sh or
tabbrew update put there; Homebrew, npm and source installs keep theirs. Before you
run it, tabbrew init --remove in any repo that carries the cheat sheet.
Then remove the extension in chrome://extensions.
Every verb takes
--jsonand is silent on success. Ids are the TAB, WINDOW and GROUP columns printed by thelistverbs.
tabbrew session start # spawn in the background, open the connection page
tabbrew session start --no-open # spawn only
tabbrew session open # (re)open the connection page
tabbrew session status # address, pid, version, uptime (exit 1 if none)
tabbrew session stop # ask it to exit, wait for the port
tabbrew session run # foreground, Ctrl-C to stopThe session exits after 10 idle minutes. TABBREW_EXTENSION_ID, or
extensionId in the nearest .tabbrew.json, opens another extension build.
tabbrew tabs list # one row per tab, every window
tabbrew tabs create https://example.com # open in the background, end of current window
tabbrew tabs create example.com --after 1901 # right after tab 1901
tabbrew tabs create --window 1843 --group 7 # new tab page, inside group 7
tabbrew tabs focus 1901 # select it and raise its window
tabbrew tabs move 1950 --after 1901 # next to 1901, across windows if needed
tabbrew tabs move 1950 1952 --before 1903 # several, in the order given
tabbrew tabs group 1901 1903 --title Work # new group, --color / --collapse optional
tabbrew tabs group 1950 --to 7 # join an existing group
tabbrew tabs ungroup 1903 # take it out of its group
tabbrew tabs discard 1950 # unload from memory, keep on the tab strip
tabbrew tabs reload 1950 # reload in place
tabbrew tabs reload 1950 --hard # bypass the cache
tabbrew tabs close 1950 1952 # close, no undofocus is the one verb that steals OS focus. discard may give a tab a new
id; --json reports it with previousTabId.
tabbrew windows list # one row per window, tab and group counts
tabbrew windows create # empty window
tabbrew windows create a.com b.com --focus # with tabs, brought to the fronttabbrew groups list # one row per group: colour, collapsed, tab count
tabbrew groups collapse 7 9 # fold, in order
tabbrew groups uncollapse 7 # expand again
tabbrew groups close 7 # close its tabs, keep the group among Chrome's saved groupsOther Chrome extensions can add commands of their own with
@tabbrew/sdk. They live under tabbrew plugin, apart
from the commands above, and appear once that extension's page has connected.
tabbrew plugin list # known plugins, and which Chrome extension serves each
tabbrew plugin --help # the same, with what each one is for
tabbrew plugin notes --help # one plugin's commands
tabbrew plugin notes add "call back" --tab-id 1901
tabbrew plugin open notes # open its page in Chrome, as `session open` does for the extension
tabbrew plugin forget notes # drop one seen in an earlier sessionA plugin's names, help text and output are written by its extension, not by
TabBrew. Every plugin command takes --json.
Plugins are remembered in ~/.tabbrew/plugins.json for 30 days, so one whose
page is closed is still a command: the CLI opens the page the plugin declared
and then calls it. A name goes to the first extension that declares it in a
session; when that is a different extension than last time, the command says
so before it runs.
tabbrew init # write the cheat sheet into CLAUDE.md / AGENTS.md / .cursorrules
tabbrew init --agent codex # one tool's file: claude, cursor, codex, all
tabbrew init --path docs/AI.md # explicit file, inside the current directory
tabbrew init --print # show the block, write nothing
tabbrew init --remove # strip the block everywhere
tabbrew init --extension harness # record which extension build to open: store, harness, or an idThe block sits between <!-- TABBREW:START --> and <!-- TABBREW:END -->
and is replaced on every run, so re-run init after upgrading.
bun install
bun start --help # run from source
bun run build # standalone binary at dist/tabbrew
./dist/tabbrew --versionBiome handles formatting, linting, and import order (biome.json).
bun run check # verify (what CI runs)
bun run check:fix # apply safe fixes
bun run typecheck
bun testCommands live in src/commands/<noun>/<verb>.ts, one verb per file; init
and update are top-level verbs. src/core/ holds the logic behind them and
never imports from src/commands/, so it can be tested without going through
the CLI.
See CONTRIBUTING.md for the branch flow and how a release is cut.
extension/ is the development harness for the CLI's browser side, not the
TabBrew product extension. Browser-facing features land here paired with their
CLI command (the session handshake, the three list verbs,
tabbrew groups collapse/uncollapse/close, tabbrew tabs create,
tabbrew tabs focus, tabbrew tabs move, tabbrew tabs discard,
tabbrew tabs reload, tabbrew tabs close and tabbrew tabs ungroup today)
so the protocol can be exercised end to end in a real Chrome. The product extension moves to its own repository once that protocol is
stable; see extension/README.md.
It is a minimal Manifest V3 extension, built with WXT and
React, that connects Chrome to the session. An open extension page is the
connection: while it is open it polls GET /health every 3 seconds, and once
a session answers it holds a long-poll on that session and serves the commands
it claims; close it and nothing runs. There are two such pages running the
same app: connection.html, a tab the CLI opens itself (tabbrew session start / open), and the side panel behind the toolbar icon, for opening by
hand. There is deliberately no background polling.
bun run build:ext # production build to extension/dist/chrome-mv3
bun run dev:ext # dev build with live reload
bun run zip:ext # extension/dist/tabbrew-extension.zip, the release assetLoad it once: chrome://extensions → Developer mode → Load unpacked →
extension/dist/chrome-mv3. The manifest pins a key, so the id Chrome
shows is the same on every machine; a build loaded before the key was pinned
must be removed and loaded again. The CLI opens the Web Store extension by
default, so point it here with TABBREW_EXTENSION_ID=<that id> or, once per
checkout, tabbrew init --extension harness; from then on
tabbrew session start opens the page, and the toolbar icon opens the side
panel instead.
bun install runs wxt prepare, which generates the TypeScript config in
extension/.wxt/; both that and dist/ are ignored by git.
The e2e suite in src/e2e/ drives the compiled CLI against a real Chrome
holding this build, with no fake panel in between. It is skipped by plain
bun test; CI runs it under Xvfb in the e2e-chrome job. Locally:
bunx @puppeteer/browsers install chrome@stable # Chrome for Testing; branded Chrome 137+ refuses --load-extension
bun run build && bun run build:ext
TABBREW_E2E_CHROME_BIN=/path/to/chrome bun run test:e2eIt uses a throwaway profile and state directory, needs the default ports free (no other session running), and closes its Chrome when done.
The ports, the service: "tabbrew-session" marker the page checks, and the
command channel's paths and shapes live in src/core/session/protocol.ts,
which both the CLI and the extension import, so the two sides cannot drift
apart. extension/wxt.config.ts also derives the manifest's
host_permissions from that list.
The page's side of all this is @tabbrew/sdk, which the harness
consumes like any other extension would. To make your own extension serve
tabbrew, start there.