
Research
/Security News
OpenAPI React Query Codegen Compromised in Mini Shai-Hulud npm Supply Chain Attack
Ten malicious OpenAPI React Query Codegen versions were published to npm in the Mini Shai-Hulud attack, all with valid provenance.
Every open tab is a thread you forgot to pull. Tabctl helps you query and change them safely.
A command-line instrument for browser tab orchestration, now centered on a GraphQL API exposed through tabctl query and tabctl schema, plus ping and history convenience commands. Built for humans who hoard tabs and the AI agents who clean up after them.
mise use -g github:ekroon/tabctl # install the tabctl binary
tabctl setup --browser edge --extension-id <id> # or: --browser chrome --extension-id <id>
# Load the extension: edge://extensions → Developer mode → Load unpacked → paste: ~/.local/state/tabctl/extension/
tabctl ping
Setup writes the wrapper script, native messaging manifest, and registers the profile in one step. Works on macOS, Linux, and Windows. If it pings back, the wire is live. You're connected.
cargo install --path rust/crates/tabctl
Legacy:
npm install -g tabctlstill works for the Node.js-based distribution but is no longer the primary install method. No Node.js or Go is required at runtime — the singletabctlbinary handles everything.
Give your coding agent eyes into the browser. Install the tabctl skill via the Skills CLI:
npx skills add https://github.com/ekroon/tabctl --skill tabctl -a opencode -a github-copilot -a claude-code
Nothing leaves your machine. No cloud. No telemetry. Just a socket between your terminal and your browser, quiet as rain on neon.
Every mutation is undoable — undoAction rewinds closes, archives, and group changes like they never happened. A configurable policy layer shields pinned tabs and protected domains from accidental destruction. You pull the trigger; tabctl keeps the safety on until you mean it.
When tabctl is installed as a skill, your agent sees what you see. Just talk to it.
"Which of my tabs can I close?" The agent scans for duplicates, stale pages, and tabs you haven't touched in days — then offers to clean house.
"Are any of my open tabs relevant to my note on Project Helios?" When connected to Obsidian, your agent cross-references every open tab against your notes and surfaces the ones that matter.
"I just finished researching service mesh architectures. Organize what I found." Groups your tabs by theme, extracts key URLs, and drops a summary into your notes — before you forget what you were looking at.
"Where's that AWS pricing page I had open somewhere?" The agent searches your open tabs and groups by title and URL — and brings it back into focus.
"Pull every error message from my open Sentry tabs into a markdown table." The agent reads each tab, extracts what you need, and formats it — no copy-paste, no context switching.
"Group everything by project. You know which ones." Your agent infers context from URLs, titles, and your workspace — then sorts ninety tabs into five groups with names that actually make sense.
tabctl is a single Rust binary that serves as both the CLI and the native messaging host. The CLI sends commands over a Unix socket (or named pipe on Windows) to the host, which proxies them to the browser extension via native messaging. The tabctl host subcommand is the native messaging entry point — invoked automatically by the browser, not manually.
This repo contains:
src/extension/, the only TypeScript component)rust/crates/*) — single tabctl binary for CLI + host + shared runtimecargo install --path rust/crates/tabctl # puts tabctl on your PATH
For development with the full build pipeline (extension + Rust):
npm install
npm run build
Local development shortcuts are available via Makefile:
make dev-up BROWSER=edge PROFILE=edge
make dev-run PROFILE=edge CMD="list --all --json"
make dev-run-release-like PROFILE=edge CMD="list --all --json"
If npm is not on PATH in your shell, override it per command:
make dev-build NPM=~/.local/share/mise/shims/npm
Run setup to write the manifest, wrapper script, and profile registration:
tabctl setup --browser chrome
This will:
profiles.jsontabctl-extension.zip + .sha256) into the tabctl data directory~/.local/state/tabctl/extension/)--extension-id)chrome://extensionsFor local dev builds (no GitHub download), point setup at an unpacked directory:
tabctl setup --browser chrome --extension-dir dist/extension
Edge? Use
--browser edgeand load fromedge://extensionsinstead.Cross-platform: setup works on macOS, Linux, and Windows. On Windows, setup verifies connectivity after writing setup artifacts and checks the runtime extension ID reported by the browser. Connectivity failures and runtime extension ID mismatches exit non-zero and print manual recovery steps (including expected vs runtime IDs).
Optional setup release overrides:
--extension-dir, --release-repo, --release-tag (or --release-version), --release-asset, --skip-extension-downloadTABCTL_SETUP_EXTENSION_DIR, TABCTL_RELEASE_REPO, TABCTL_RELEASE_TAG, TABCTL_RELEASE_ASSET, TABCTL_SETUP_FETCH_EXTENSION=0tabctl ping
tabctl query '{ tabs { total items { tabId title url } } }'
tabctl schema
Multiple browsers? See Multi-Browser Setup for running tabctl with both Chrome and Edge.
| Command | Description |
|---|---|
tabctl query '<GRAPHQL>' | Query and mutate browser state through GraphQL |
tabctl schema | Print the GraphQL schema |
tabctl ping | Check host/browser connectivity and runtime version sync |
tabctl history | Show recent undo history entries |
tabctl setup, doctor, policy, profile-* | Local/admin profile management |
See CLI.md for the full command reference, options, and examples.
# Query tabs and groups
tabctl query '{ windows { windowId groups { groupId title } tabs { tabId title url groupTitle } } }'
# Analyze stale and duplicate tabs
tabctl query '{ analyze(windowId: 123, staleDays: 30) { totalTabs duplicateTabs staleTabs } }'
# Inspect page metadata
tabctl query 'query { inspectTabs(tabIds: [456], signals: ["page-meta"]) { entries { tabId signals { name valueJson } } } }'
# Generate reports
tabctl query '{ reportTabs(windowId: 123) { entries { tabId title url description } } }'
# Capture screenshots
tabctl query 'query { captureScreenshots(tabIds: [456], mode: "viewport") { entries { tabId tiles { index width height } } } }'
# Inspect persisted browser-state history for future restore tooling
tabctl query 'query { latestBrowserState { snapshotId reason groups { logicalGroupId title browserGroupId tabUrls } } }'
tabctl query 'query { browserStateHistory(limit: 10) { snapshotId recordedAt reason eventCount eventKinds } }'
tabctl query 'query { browserStateGroupHistory(title: "Research", limit: 10) { snapshotId logicalGroupId title browserGroupId tabUrls } }'
# Open tabs in a new grouped window
tabctl query 'mutation { openTabs(urls: ["https://example.com"], group: "Research", newWindow: true) { windowId groupId tabs { tabId url } } }'
# Close tabs with undo support
tabctl query 'mutation { closeTabs(tabIds: [456], confirm: true) { txid closedTabs } }'
tabctl query 'mutation { undoAction(latest: true) { txid summary } }'
Install the tabctl skill for agents (OpenCode, Claude Code, Codex, etc.) via the Skills CLI:
npx skills add https://github.com/ekroon/tabctl --skill tabctl -a opencode
Install globally:
npx skills add https://github.com/ekroon/tabctl --skill tabctl --global -a opencode
By default the CLI loads a policy file from:
<configDir>/policy.json (default: ~/.config/tabctl/policy.json)
Set TABCTL_CONFIG_DIR to override the config directory.
This is a protection-only policy that marks tabs as ineligible for agent actions. Example:
{
"protect": {
"pinned": true,
"groupTitles": ["🔒"]
}
}
Create a default policy file:
tabctl policy --init
tabctl setup does not install a default policy.
See config/policy.example.json for a starter template.
Config directory: TABCTL_CONFIG_DIR → $XDG_CONFIG_HOME/tabctl → ~/.config/tabctl
An optional config.json in the config directory can set dataDir to override where state files (socket, undo log) are stored. When TABCTL_CONFIG_DIR is set but no dataDir is configured, data defaults to <configDir>/data/; otherwise it uses $XDG_STATE_HOME/tabctl (or ~/.local/state/tabctl).
See CLI.md for full details.
<dataDir>/tabctl.sock (default: ~/.local/state/tabctl/tabctl.sock)<dataDir>/undo.jsonl (default: ~/.local/state/tabctl/undo.jsonl)<dataDir>/state.db (default: ~/.local/state/tabctl/state.db)<configDir>/profiles.json<dataDir>/pipe-endpointOn Windows, the host exposes a named-pipe endpoint model:
\\.\pipe\tabctl-<hash>).<dataDir>/pipe-endpoint, and the WSL CLI relays through powershell.exe.WSL endpoint discovery (CLI):
TABCTL_SOCKET (explicit endpoint).pipe-endpoint file discovery from resolved data dir (and equivalent /mnt/c/Users/*/.../tabctl/.../pipe-endpoint locations).WSL named-pipe mode:
<dataDir>/pipe-endpoint (including the mirrored /mnt/c/Users/*/... candidate paths used for other WSL bridge files).Relevant knobs: TABCTL_SOCKET, TABCTL_PROFILE, TABCTL_DATA_DIR, TABCTL_STATE_DIR, TABCTL_CONFIG_DIR.
tabctl setup fails with Windows setup verification failed: check data.verification.reason in JSON output (ping-timeout, socket-not-found, socket-refused, ping-not-ok, extension-id-mismatch), then follow printed manual steps.extension-id-mismatch): compare expected vs runtime IDs from setup output, then rerun setup with the runtime ID shown by edge://extensions / chrome://extensions:
tabctl setup --browser <edge|chrome> --extension-id <runtime-id>tabctl query 'mutation { reloadExtension { reloading } }' if the browser does not pick up changes immediately.TABCTL_AUTO_SYNC_MODE=release-like.TABCTL_AUTO_SYNC_MODE=off.tabctl ping --json is a host connectivity/health check; use it to confirm the native host is reachable and healthy.versionsInSync, hostBaseVersion, and baseVersion.tabctl ping returns connect errors (ENOENT, ECONNREFUSED, timeout): ensure extension is loaded and active, rerun tabctl setup, and in WSL verify the profile data dir contains a current pipe-endpoint file.tabctl doctor --fix --json includes per-profile connectivity diagnostics in data.profiles[].connectivity; if ping remains unhealthy after local repairs, follow manualSteps.Local release-like sync test recipe:
# 1) Install an older extension release into managed extension path
tabctl setup --browser edge --extension-id <extension-id> --release-tag v0.5.2
# 2) Run the current binary with forced release-like auto-sync
TABCTL_AUTO_SYNC_MODE=release-like cargo run --manifest-path rust/Cargo.toml -p tabctl -- query '{ tabs { total } }'
# 3) Verify host connectivity/health after auto-sync
tabctl ping --json
Advanced topic — you only need this if you run tabctl with more than one browser (e.g. Edge and Chrome).
tabctl supports multiple browser profiles. Each profile connects to a different browser (Chrome, Edge).
# Setup for Edge
tabctl setup --browser edge --extension-id <edge-extension-id>
# Setup for Chrome (with custom name)
tabctl setup --browser chrome --name chrome-work --extension-id <chrome-extension-id>
# List profiles
tabctl profile-list
# Switch default
tabctl profile-switch edge
# One-off command with different profile
tabctl --profile chrome-work query '{ tabs { total } }'
If you launch Chrome with --user-data-dir, Chrome looks for native messaging manifests inside that directory. Use --user-data-dir in setup to write the manifest to the right place:
tabctl setup --browser chrome --user-data-dir /path/to/chrome-profile
This writes the manifest to <user-data-dir>/NativeMessagingHosts/ instead of the system-wide location.
Each profile gets its own:
Policy is shared across all profiles.
powershell.exe; no TCP fallback is used on that path.The single tabctl binary is built from the Rust workspace (rust/). TypeScript is limited to the browser extension boundary (src/extension/). No Node.js or Go is required at runtime.
Build and verify:
cargo build --release -p tabctl # build the binary
npm install && npm run build # full pipeline (extension + Rust)
npm test # unit tests
Rust-only validation:
npm run rust:verify
npm run check:targets # local cross-target cfg/type check
On macOS, npm run check:targets can use Zig for the C cross-compiler needed
by libsqlite3-sys:
brew install zig
The script auto-detects Zig outside CI and wires the Linux/Windows C compiler environment for the check. The pre-push hook does not run this optional check; run it manually when you want local cross-target coverage.
Browser-backed integration harness (requires built dist artifacts and Chrome):
npm run test:integration
WSL CI validates the WSL->Windows invocation bridge (test.yml wsl job) with phases: prerequisites, diagnostics, build_and_unit, setup_validation, windows_invocation, integration. Runtime/build execution is delegated to Windows commands (cmd.exe/powershell.exe), so WSL-local Rust compilation is not required.
The base version lives in package.json and is embedded into the CLI, host, and extension at build time.
Commands:
npm run bump:patch
npm run bump:minor
npm run bump:major
npm run bump:alpha
npm run bump:rc
npm run bump:stable
Pre-release staging flow:
bump:alpha creates/increments x.y.z-alpha.Nbump:rc promotes alpha to x.y.z-rc.1 (or increments RC)bump:stable drops the prerelease suffix for final stable publishRelease automation:
v<version> and dispatches Release.package.json version and optionalDependencies.tabctl-win32-x64 are kept in sync by scripts/bump-version.js.Release publishing (.github/workflows/release.yml) supports both tag pushes and explicit workflow dispatch, and enforces:
package.json version (v<version>)package.json.optionalDependencies["tabctl-win32-x64"] must match package.json versionalpha/rc; stable publishes to latestnpm run build and npm test must pass before publishtabctl-extension.zip plus tabctl-extension.zip.sha256Fetch the extension asset from a release with:
tabctl extension-fetch --version 0.5.3
Local builds default to a dev version when a .git directory is present, appending the short SHA.
npm run build
This produces versions like 0.1.0-dev.abc12345 (and appends .dirty when the repo has uncommitted changes).
For release builds without SHA, set:
TABCTL_VERSION_MODE=release npm run build
Notes:
tabctl query.inspectTabs supports page-meta and selector signals.captureScreenshots returns tile metadata and image data from GraphQL.undoAction accepts either an explicit txid or latest: true.tabctl history --json returns a top-level JSON array.FAQs
CLI tool to manage and analyze browser tabs
The npm package tabctl receives a total of 4 weekly downloads. As such, tabctl popularity was classified as not popular.
We found that tabctl demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Research
/Security News
Ten malicious OpenAPI React Query Codegen versions were published to npm in the Mini Shai-Hulud attack, all with valid provenance.

Security News
Socket joins more than 100 technology, cybersecurity, and financial organizations calling for a global surge in cyber defense.

Product
Enterprise security teams can now detect malware, credential theft, suspicious network activity, and risky updates across Microsoft Edge extensions.