A command line for driving Stylebot in a running browser: list tabs, read and inspect pages, hover and pick elements, force hover and focus states, get and set styles, and take screenshots. It's meant for scripting and for coding agents such as Claude Code, Codex or Cursor, which can restyle a page and check the result without the editor open.
It works with Stylebot on Chrome, Edge and the other Chromium browsers listed below, on macOS, Linux and Windows, once turned on in Stylebot's options. Firefox and Safari aren't supported yet.
The CLI needs Node 20 or later, and Stylebot 4 or later in the browser. It's published to npm as @stylebot/cli:
npm install -g @stylebot/cli
stylebot installTo run this checkout's CLI instead, use yarn stylebot inside the repo. To run it as stylebot from anywhere on macOS or Linux, link it onto your PATH:
ln -s "$PWD/tools/cli/bin/stylebot.mjs" ~/bin/stylebotOn Windows, run yarn stylebot or node tools\cli\lib\cli.mjs.
The first command sets the CLI up with your browsers on its own, saying which. To check setup, or after installing another browser, run install; for this checkout's CLI, run it from the repo so it finds the dev profiles:
yarn stylebot installThis copies the CLI's native messaging host into ~/.stylebot/ and registers it with each installed browser, then prints a checklist of what's done and what's left: the browsers it found, whether Stylebot is in one of them, and whether it's connected. Run it again any time to check. On macOS and Linux it also registers with the dev profiles yarn dev:chrome launches, which keep their own list of hosts; on Windows the browser's registration covers them.
| Browser | macOS | Linux | Windows |
|---|---|---|---|
| Chrome | ✓ | ✓ | ✓ |
| Edge | ✓ | ✓ | ✓ |
| Brave | ✓ | ✓ | ✓ |
| Chromium | ✓ | ✓ | ✓ |
| Vivaldi | ✓ | ✓ | ✓ |
| Arc | ✓ |
A browser counts as installed once it has run, which leaves a Local State file in its user data folder, so run install again after installing a new browser.
Then turn on Allow command line access in Stylebot's options, under Basics, and allow what the browser asks for. The extension connects to the host while the setting is on; turning it off disconnects it and gives back the permission to talk to the host. If it was already on before you installed, reload the extension or restart the browser. The host runs only while the browser does.
A command that can't reach the host works out why and prints one fix:
- No browser could be set up: install Chrome or Edge.
- No browser profile has Stylebot: add it. The CLI looks for Stylebot's store ids in each profile's extensions and preferences, which covers unpacked dev builds too, and says this only when it found profiles to look in.
- The host is registered but no browser has ever started it, so the setting is almost certainly off: turn it on. The host leaves a timestamp in
~/.stylebot/each time a browser starts it, one per socket, so a test browser's doesn't count for yours. - A browser has started it before: the browser is closed, or Stylebot isn't running in it. Open it.
The Stylebot skill relays these messages to you as they are.
stylebot --help lists every command, and stylebot <command> --help describes one.
Commands that only read default to the active tab, or take --tab when their arguments are selectors. Commands that change a style, take a screenshot, move the pointer or force a state (css set, css rule, css add, profile, screenshot, hover, inspect, force) need the site or tab named, because an agent runs its commands over several steps and the active tab can change in between.
- A tab is a tab id, from
tabsoropen. - A target is a tab id or a site, such as
news.ycombinator.com. - A profile is named by its name or id.
Each browser running Stylebot with the setting on connects on its own, and so does each profile of one browser. With one connected, commands go to it. With more, every command takes --browser to say which, since each keeps its own styles and tabs:
yarn stylebot browsers
yarn stylebot open news.ycombinator.com --browser edgebrowserslists them, each with the page it shows, to tell them apart. A browser goes by its name, such aschromeoredge, or by its name and a short id, such aschrome-k3f9, when two of its profiles are connected.- A command run on a terminal without
--browserasks which browser to use. Run elsewhere, such as by an agent, it fails, listing the browsers to choose from. The/stylebotskill then asks you which one, and names it in every command after.
stylebot open news.ycombinator.com
stylebot outline 54432929
stylebot screenshot 54432929 -o page.png
stylebot screenshot 54432929 --selector '.titleline > a' -o link.png
stylebot screenshot 54432929 --clip 0,0,600,400 --scale 2 -o corner.png
stylebot screenshot 54432929 --state hover --on '.athing' --selector '.athing' -o row.png
stylebot doneopenshows the page in the CLI's own window, which opens behind yours without taking focus, so an agent never switches your tabs. It reuses that window's tab for the page (any page on the site, for a bare site);--newalways opens another.tabslists open tabs, starring the one each window shows.outlineprints the page's visible elements as an indented outline.screenshotsaves a PNG of a tab's viewport. A hidden tab is shown for a moment to capture it, then the window switches back. It prints the file, the image's size and its scale.--selectorkeeps only the first visible element a selector matches, with a few pixels around it. One out of view is scrolled to for the capture, and the page scrolled back after. One bigger than the viewport is cut to the part that fits, and the output says so. Checking one element this way is quicker and smaller than a whole page.--clip x,y,w,hkeeps a region, in CSS pixels of the viewport. It can't be combined with--selector.--scalesets image pixels per CSS pixel. The default, 1, makes a point in a full screenshot the CSS pixelhoverandinspecttake; 2 keeps a 2× screen's full resolution.--state <state> --on '<selector>'forces a state on the selector's first visible match for this capture only, asforcedoes, then takes it off again, unless it was already forced.
donecloses the CLI's window.
stylebot suggestions 54432929
stylebot css-variables 54432929
stylebot page-rules '.titleline > a' --tab 54432929
stylebot computed-styles body color font-size --tab 54432929
stylebot match-count '.titleline > a' '.subline' --tab 54432929These read the page so an agent can look at a page before styling it without another browser tool.
suggestionsprints styling requests for the page: up to two that suit it, and a creative look.css-variablesprints the page's css variables, as the:rootandbodyrules that set them, so a style can override them.page-rulesprints the page's own rules for the elements a selector matches (the first five). Cross-origin stylesheets can't be read, so they're only counted.computed-stylesprints computed values on the first element a selector matches: the properties named, or common layout, color and type ones.match-countprints how many elements each selector matches, andinvalidfor one that doesn't parse.
stylebot hover 54432929 '#nav .menu'
stylebot inspect 54432929 120,340These point at the page, so an agent can open what only shows on hover, and find the selector for an element it sees in a screenshot. Neither clicks anything or draws on the page, so screenshots stay clean. Each needs the tab named.
hovermoves the pointer onto an element, or a point given asx,yin CSS pixels of the viewport, as read off a fullscreenshotat the default scale. A selector aims at the middle of its first visible match, scrolled into view.inspectprints the selector Stylebot's inspector would pick for an element or point, preferring one the site's style already has rules for. Below it come the other selectors the editor's selector menu would offer for the element, each with how many elements it matches, so an agent can pick a narrower or broader one. It sends the page no events and doesn't open the editor.
Hover's events are synthetic: the page's own handlers run, but CSS :hover doesn't apply, and a site that checks for a real user can ignore them. To see hover styles, force the state.
stylebot force 54432929 '.athing' hover
stylebot force 54432929 'input[name=q]' focus-visible
stylebot force 54432929
stylebot force 54432929 --clearforce <tab> '<selector>' <state> makes the page's CSS for a state apply as if a real pointer or focus were there: hover, active, focus, focus-visible or focus-within. It goes on the selector's first visible match, or every match with --all, and stays until --clear. While it's forced, outline, computed-styles, page-rules, inspect, the page check after a save and screenshot all see the page in that state, so an agent can check a theme's hover and focus colors instead of reading them off the CSS. force <tab> lists what's forced.
$ stylebot force 1234 '.athing' hover
Forced hover on .athing (1 element, 6 ancestors).
It works the way Storybook's pseudo-states addon does, without the browser's debugger, so it needs no extra permission:
- It copies every rule on the page that names one of those states, from the page's stylesheets, its constructed ones and those of open shadow roots, with the state swapped for an attribute of the same specificity, inside the same
@media,@supports,@layerand@container,!importantkept. The copies go after the scope's own sheets. A cross-origin stylesheet the page can't read is fetched by the extension, which says when it couldn't fetch one. - Hover and active go on the element and every ancestor, across shadow boundaries; a focus state goes on the element, with focus-within on its ancestors. Transitions the change starts jump to their end, so a screenshot shows the state rather than part of the fade.
- While anything is forced, it copies the rules again when stylesheets are added or change, including Stylebot's own when a save applies, and before every command reads the page.
Its limits:
- Only the top frame is forced, as the CLI only reads that one; iframes keep their real state.
- A forced state doesn't stop the page's rules for when an element isn't in that state, such as
.button:not(:hover), from applying to it too. - The copies come after every other rule, so where a state rule and a later rule tie on specificity without
!important, the state rule wins, where with a real pointer the later one would. Stylebot's own rules carry!importantunless the site turns that off, so they still win. - An element the page replaces, as a framework re-render can, loses its forced state;
forceagain.
stylebot css get news.ycombinator.com --grep titleline
stylebot css rule 54432929 '.titleline a' --set color=#e8e6e3
stylebot css add 54432929 --file more.css
stylebot css set 54432929 < hn.css-
styleslists saved styles. -
css getprints a style's css.--grep <text>prints only the rules whose selector or declarations contain the text, ignoring case, inside the at-rules they sit in. -
css setsaves css from stdin or--filein place of the whole stylesheet and applies it to open tabs right away. Empty css deletes the style. -
css rule <target> '<selector>'edits the first top-level rule with exactly that selector, or with--media '<query>'the first inside that top-level@media.--set property=valuesets a declaration and--unset propertyremoves one, each repeatable;--deleteremoves the rule. Setting creates the rule at the end when none matches, and a rule left empty is removed. Everything else in the stylesheet keeps its formatting and comments. -
css addadds css from stdin or--fileto the end of the stylesheet, moving any@importto the top, and names the rules it added whose selector already has one, whichcss rulecould have edited instead. -
css diffshows the rules css from stdin or--filewould add, change and remove ifcss setsaved it, without saving anything. -
css ruleandcss addsave the waycss setdoes, with the same checks and report:-
It refuses css that doesn't parse, saying at which line, and adds Google Fonts imports for the families the css names. It also saves a selector's partly generated class names by their stable part, and says which it changed.
-
When the profile it saves is the one in use and an open tab shows the site, it checks the page; otherwise it says why it didn't. Given a tab id, it saves the style that applies on that tab and checks that tab. Given a site, it checks the active tab of the CLI's own window if it shows the site, else any tab that does, and names the other tabs showing the site in one line.
$ stylebot css set news.ycombinator.com --file hn.css Saved news.ycombinator.com Checked on tab 1234. Also shown on tab 1240: name a tab id instead of the site to check that tab. Elements each added or changed rule matched: - .titleline a: 30 elements Removed rules: - .old-banner 5 unchanged rules not listed Problems the page check found: - Hard to read: .subtext (30 elements), text #828282 on #1d1f21, contrast 3.1:1 -
css rulesays when it created or removed the rule, and names the properties it was asked to unset that the rule didn't have.$ stylebot css rule 1234 '.subtext' --set color=#a8a8a8 Saved news.ycombinator.com Checked on tab 1234. Elements each added or changed rule matched: - .subtext: 30 elements 5 unchanged rules not listed -
css diffprints each rule added or removed whole, and for each rule changed only the declarations that differ:$ stylebot css diff news.ycombinator.com --file hn.css --- news.ycombinator.com, as saved +++ the css given .subtext { - color: #828282; + color: #a8a8a8; } + .comment (in @media (max-width: 750px)) { + font-size: 15px; + } Rules: 1 added, 1 changed, 0 removed. Nothing saved.
-
stylebot profile create Dark news.ycombinator.com --use
stylebot css set news.ycombinator.com --profile Dark < dark.cssprofileslists a style's profiles, starring the one in use.profile create,use,renameanddeletemanage them.css get,rule,add,setanddifftake--profileto name the profile rather than use the active one. The/stylebotskill always passes it, so a profile switch mid-session can't send its css into your own style.
stylebot options news.ycombinator.com
stylebot option set scheme dark news.ycombinator.comoptionslists the options a profile's css declares (see Style options), each with its type, its label when the author gave one, its value, the values it takes and its default. A checkbox takes0and1.option setsets one, applying it to open tabs as the popup does. It refuses an option the profile doesn't declare, or a value it doesn't take.- Both take
--profileto name the profile rather than use the active one. The/stylebotskill usesoption setto check a light/dark style in each variant, then sets it back to the user's choice.
Every command takes --json to print the raw response instead of text.
CLI mode lets you work with your agent from the page. A small terminal on a connected tab shows each command the agent runs there and each save, with an undo. You mark areas of the page and write notes about them; the agent makes the change and answers in the terminal.
- Connect: ask your agent to watch, then click [connect] on the tab you're looking at, or let the agent connect one with
mode <tab>. One tab is connected at a time; others offer [move here]. The terminal's close button ormode <tab> --offdisconnects it. - Mark: drag over part of the page. Click a mark's numbered badge to remove it.
- Note: type in the terminal and press Enter (Shift+Enter for a new line). The agent gets the note with a screenshot of your marks, and shows as thinking until it answers.
Keys, anywhere on the page but a field: . open or minimize, / write a note, Z undo the last save, X clear the marks.
The agent follows along with watch, which prints a line for each event (the tab attaching, each mark and note, navigations, edits outside the CLI), or selection, which lists the notes and marks sent since it last looked, with the elements inside each mark; --wait waits for the next one. say answers your note. A session might go:
$ stylebot watch
waiting for a tab: ask the user to connect the tab to work on with the connect button Stylebot shows on it, or run: stylebot mode <tab>
attached tab 54432929 https://news.ycombinator.com/
mark ① area 640×48
note "keep this bar on screen when I scroll" about ① (screenshot /Users/you/.stylebot/work/stylebot-note-54432929-1.png)
$ stylebot css rule 54432929 '#hnmain > tbody > tr:first-child' --set position=sticky --set top=0 --profile Default
$ stylebot say 54432929 'The top bar now stays at the top as you scroll.'
The Stylebot skill teaches a coding agent to restyle pages with the CLI: open the page, read its outline, write CSS, fix what the page check reports, then look at a screenshot. It works in any agent with a shell and a file-writing tool, running on the same machine as the browser, once the setup above is done. Ask for a look, such as "give Hacker News a dark theme", and the agent picks the skill up on its own; or invoke it by name. Without saying what to change, it offers suggestions for the page.
The skill keeps its CSS and screenshots in ~/.stylebot/work/. When you name a profile, it updates that one, and in CLI mode the one you're using; otherwise its changes go into a new profile named after the agent and the look, such as "Claude: Dark", so switching back to your own style is one profile change.
Agents with plugin marketplaces install it from this repo's, as a plugin whose stylebot command runs the CLI on your PATH, or the published one through npx when there's none. The others copy the skill's folder, and need the CLI installed with npm install -g @stylebot/cli.
| Agent | How | Checked |
|---|---|---|
| Claude Code | This repo's marketplace | Restyling measured by the styling evals |
| Codex CLI | This repo's marketplace | Installed and its skill listed, Codex 0.163.0 |
| Copilot CLI | This repo's marketplace | Installed and its skill loaded, Copilot 1.0.99-2 |
| Copilot (VS Code) | Copy the skill | From VS Code's docs only |
| Cursor | Copy the skill | From Cursor's docs only |
| Antigravity CLI | Copy the skill | From Antigravity's docs only |
How well an agent restyles pages is only measured with Claude Code (see docs/styling-evals.md); the others were checked for installing the skill and finding it, by the author of #1223. Each agent's commands and folders below follow its own docs as of October 2026.
claude plugin marketplace add ankit/stylebotclaude plugin install stylebotUse ./ in place of ankit/stylebot to install this checkout's plugin, or try it in one session with claude --plugin-dir tools/agent-plugin. Invoke it with /stylebot <site> <what to change>. It runs without permission prompts: the skill pre-approves stylebot commands and reading and writing files in ~/.stylebot/work/.
codex plugin marketplace add ankit/stylebotcodex plugin add stylebot@stylebotIn a new session, ask for a look, or pick stylebot from /skills. To run stylebot commands without approving each one, choose to always allow them at the first prompt, or add a rule to ~/.codex/rules/default.rules:
prefix_rule(pattern = ["stylebot"], decision = "allow")copilot plugin marketplace add ankit/stylebotcopilot plugin install stylebot@stylebotcopilot skill list shows whether the skill loaded. Copilot doesn't keep command approvals between sessions: approve stylebot for the rest of the session at the first prompt, or start it with copilot --allow-tool='shell(stylebot:*)'.
From this checkout, copy the skill folder into the agent's personal skills folder, then restart the agent:
| Agent | Personal skills folder | Project skills folder |
|---|---|---|
| Copilot (VS Code) | ~/.copilot/skills/ |
.github/skills/ |
| Cursor | ~/.cursor/skills/ |
.cursor/skills/ |
| Antigravity CLI | ~/.gemini/antigravity-cli/skills/ |
.agents/skills/ |
cp -R tools/agent-plugin/skills/stylebot ~/.cursor/skills/Copilot CLI reads ~/.copilot/skills/ too, and Cursor also reads ~/.claude/skills/. Ask for a look, or type /stylebot.
To run stylebot commands without a prompt each time:
- Copilot in VS Code: add
"chat.tools.terminal.autoApprove": { "stylebot": true }to your user settings. - Cursor: pick Allowlist under Cursor Settings > Agents > Approvals & Execution, and add
stylebotto the allowlist, or put{ "terminalAllowlist": ["stylebot"] }in~/.cursor/permissions.json, which replaces the allowlist in settings. - Antigravity CLI: choose to always allow
stylebotat the first prompt, or add"permissions": { "allow": ["command(stylebot)"] }to~/.gemini/antigravity-cli/settings.json.
A test, or a second worktree, can run its own dev browser beside yours. It connects like any other browser, so the CLI asks which to use. To keep it apart entirely, give it its own profile and socket:
STYLEBOT_PROFILE_DIR=.chrome-dev-profile-test STYLEBOT_SOCKET=~/.stylebot/test.sock yarn start:chromeRun stylebot install once the profile folder exists, so it's registered there too, and run the CLI with the same STYLEBOT_SOCKET to drive that browser. With it set, the CLI sees only that browser, and without it, never sees it.
To try the CLI against a release-like build, run yarn preview:chrome instead of yarn start:chrome. It carries the store's key, so it gets the extension id the host allows.
The browser starts the native host when the extension connects to it, and keeps it running while the connection is open. Each browser profile with Stylebot connected starts its own host. Each host listens on its own Unix socket in ~/.stylebot/browsers/, readable only by you, and relays each CLI command to the extension and its answer back.
The CLI finds the browsers through their sockets. With more than one, it asks each extension which browser it runs in and what page that browser shows, to name them for --browser and to ask you which to use. A socket nothing listens on, left by a host that quit without cleaning up, is removed. A host started before each browser had its own socket listens on the one all browsers used to share, and the CLI finds it there too until the browser restarts.
The code that reads a page loads into it the first time the CLI asks about that page, so pages the CLI never touches don't carry it. It runs in the top frame only.
The extension connects only while the setting is on and the browser has granted its optional nativeMessaging permission for the host. Screenshots need <all_urls>, which Stylebot already holds on Chrome and Edge, to restore open tabs after an update.
Chrome lets the background reach the native host only once it starts again after the grant, so turning the setting on restarts the extension once. A restart cuts off the content scripts already running in open tabs, and Chrome only injects into pages that load afterwards. So after the restart Stylebot runs its content scripts again in the open tabs, as it does after an update. The fresh copy removes the panel the old one left, which no longer responds.
After it's published, the extension updates from the browser's store and the CLI from npm, so they can drift apart. Each request carries the CLI's protocol number and version, and each response the extension's. They work together only when the protocol numbers are equal: the extension refuses a request in another protocol without running it, and the CLI says which side to update, the CLI with npm or Stylebot in the browser. An extension that sends no protocol predates the check, so the CLI asks for Stylebot to be updated. The check rides on every command, with no extra round trip.
Bump the protocol, in the CLI and the extension together, when a change to a command's arguments or result would break the other side: renaming or removing a command or a field, or changing what one means. Adding a command or an optional field doesn't need a bump.
The host copy in ~/.stylebot/ records the CLI version that installed it. When a different version of the CLI runs, it copies the host again first, and when the node the launcher is pinned to is gone, such as after a node upgrade, it pins the launcher to its own. Both happen quietly before the command runs. A browser keeps running the host it started until the extension reconnects, which is harmless while the host only relays; after a repin, though, the browser couldn't start the host at all, so the CLI asks for a browser restart if it then can't connect.
Windows has no Unix sockets, so there each host listens on a named pipe for your user, and a STYLEBOT_SOCKET path names a pipe after its file. A pipe can't be taken over, so with STYLEBOT_SOCKET set, the first browser to connect keeps it until it quits.
Browsers find the host through a manifest that names its launcher and allows Stylebot's Chrome Web Store and Edge Add-ons ids. On macOS and Linux the manifest goes in a NativeMessagingHosts folder in the browser's user data folder, and the dev profiles get their own. On Windows one manifest in ~/.stylebot/ is registered under a NativeMessagingHosts key in HKCU\Software.
- macOS:
~/Library/Application Support/Google/Chrome - Linux:
~/.config/google-chrome - Windows:
HKCU\Software\Google\Chrome
- macOS:
~/Library/Application Support/Microsoft Edge - Linux:
~/.config/microsoft-edge - Windows:
HKCU\Software\Microsoft\Edge
Brave reads Chrome's hosts on macOS and Windows, so it shares Chrome's registration there.
- macOS:
~/Library/Application Support/Google/Chrome - Linux:
~/.config/BraveSoftware/Brave-Browser - Windows:
HKCU\Software\Google\Chrome
- macOS:
~/Library/Application Support/Chromium - Linux:
~/.config/chromium - Windows:
HKCU\Software\Chromium
Vivaldi reads Chrome's key on Windows.
- macOS:
~/Library/Application Support/Vivaldi - Linux:
~/.config/vivaldi - Windows:
HKCU\Software\Google\Chrome
- macOS:
~/Library/Application Support/Arc/User Data
- Publishing the CLI to npm, with the v4 store release, and the plugin through a public marketplace
- Install support for Firefox
- A visible sign that the CLI is connected, since any local process can use the socket