Command-line tool to list, switch, close, and open tabs across Firefox, Chrome, and other browsers.
tabctl-promo.mp4
Control browser tabs from the command line using D-Bus IPC.
- D-Bus Architecture - Fast, reliable inter-process communication
- Multi-Browser Support - Firefox, Zen, Chrome, Chromium, Brave, Brave Origin, and Helium work simultaneously
- Core Commands - List, close, activate, navigate, and open tabs across browsers
- Desktop Switching - Automatic window focus across virtual desktops
- Multiple Output Formats - TSV, JSON, simple
- Clean Architecture - Minimal dependencies, production ready
- Go 1.24+ (to build from source)
- D-Bus session bus
- Browser with native messaging support
yay -S tabctl
# or
paru -S tabctlAfter installation, set up native messaging:
tabctl install- Build the binaries:
git clone https://github.com/slastra/tabctl.git
cd tabctl
make buildBoth binaries land in build/. Use make build rather than a bare go build:
it stamps the version, which tabctl status reports, and it keeps every build
in one place. The native messaging manifest records an absolute path to the
mediator, so binaries in two locations means the browser can keep launching an
old one after you rebuild.
- Install native messaging host:
./build/tabctl install- Install the browser extension:
- Firefox / Zen: https://addons.mozilla.org/en-US/firefox/addon/tabctl1/
- Chrome / Chromium / Brave / Brave Origin / Helium: https://chromewebstore.google.com/detail/tabctl/baomblllgemcgbignhpbipgiofmjdhpn
- Restart browser to activate native messaging
tabctl uninstall removes the native-messaging manifest from every supported
browser, or one with --browser. Run it before removing the package; the
manifests live in your home directory, so the package manager cannot remove
them. Remove the extension from the browser itself.
# List all tabs from all browsers
tabctl list
# List tabs from specific browser
tabctl list --browser Firefox
tabctl list --browser Brave
# Activate a tab (switches desktop if needed!)
tabctl activate firefox.1.2 # Firefox tab
tabctl activate brave.1234.5678 # Brave tab
tabctl activate helium.1874583011.1874583012 # Helium tab
# Load a URL in an existing tab (keeps its position, pin and history)
tabctl navigate firefox.1.2 https://example.com
tabctl navigate firefox.1.2 https://example.com && tabctl activate --focused firefox.1.2
# Close tabs
tabctl close firefox.1.2 firefox.1.3
echo "brave.1234.5678" | tabctl close
# Open URLs in new tabs (prints the new tab IDs)
tabctl open https://example.com https://github.com
echo "https://example.com" | tabctl open
tabctl open --browser Firefox https://example.com # when multiple browsers are connected
# Show mediator/extension versions and protocol compatibility
tabctl statusTab IDs are prefixed with the lowercased browser name so multiple browsers (e.g. Brave + Helium) stay distinguishable:
<browser>.<window_id>.<tab_id>- Examples:
firefox.1.2,helium.1874583011.1874583012,brave.999.42,chrome.123.45
Release channels and editions count as separate browsers, since they install
side by side with their own profiles: bravebeta.999.42,
braveorigin.1207522592.1207522593.
Tab IDs are ephemeral. list prints them and activate, navigate or
close consume them. Do not store them.
# JSON output
tabctl list --format json
# Simple format (titles only; cannot be mapped back to tab IDs)
tabctl list --format simple
# Custom delimiter
tabctl list --delimiter ","docs/usage.md covers several profiles of one browser and the
favIconUrl field in JSON output. docs/troubleshooting.md
covers "No browsers found on D-Bus" and other failures.
Building on tabctl? COMPATIBILITY.md lists what stays stable across releases (output formats, exit statuses, tab IDs, the D-Bus interface, release asset names) and what may change.
Tools built on tabctl, including tab switchers and launchers:
- tabstrip. A tab strip daemon for
Hyprland. It shows the tabs of every browser window on the current
workspace as clickable chips with favicons. It reads tabctl over D-Bus and
updates live on the
TabsUpdatedsignal. It feeds waybar and Quickshell from the same snapshot. - lastshell. A Quickshell desktop shell whose bottom bar renders the tabstrip snapshot as a live tab strip, clicks and scroll included.
- vicinae-tabctl. A Vicinae launcher extension for switching tabs through tabctl.
- dms-tabs-launcher. A DankMaterialShell plugin that lists and activates tabs, grouped by browser.
Browser Extension ← Native Messaging → tabctl-mediator ← D-Bus → tabctl CLI
- tabctl - Command-line interface
- tabctl-mediator - Native messaging host with D-Bus server
- Browser Extensions - Firefox (Manifest V2) and Chrome (Manifest V3) extensions
- D-Bus Services -
dev.slastra.TabCtl.Firefox,dev.slastra.TabCtl.Brave
The native-messaging protocol is a JSON-RPC 2.0 subset with a version
handshake. Because the extension updates through the browser stores and the
tabctl binaries update through the AUR, the two can briefly be out of
sync after a release. Update both to matching versions together.
Both sides surface a mismatch so it never fails silently:
- If the mediator is newer,
tabctl statusreports it and any tab command fails with a clear "update" error. - If the extension is newer (it handshakes but the mediator is too old to answer), the extension shows a red ! badge on its toolbar icon. Hover it for the "update the tabctl package" hint.
ARCHITECTURE.md has the full design.
make buildA bare go build works too, but it writes wherever you point -o and skips the
version stamping, so tabctl status reports dev. Keep every build in build/
if you have a mediator installed: the native messaging manifest holds an
absolute path, so a second copy elsewhere means the browser can keep launching
the old one.
go test ./...
make test # Go tests, race tests and the extension tests
make test-integration # end-to-end, needs dbus and python3
make test-contract # CLI, D-Bus and wire output against testdata/contract
make lintInspired by BroTab, rewritten in Go with a D-Bus architecture and a Manifest v3 Chrome extension.
MIT © 2025 Shaun Lastra. See LICENSE.