DOMShell

by apireno

Not rated
GitHub

About

Browse the web with filesystem commands. 38 MCP tools let AI agents ls, cd, grep, click, and type through Chrome via a Chrome Extension.

Details

Author
apireno
Categories
Web Scraping

Setup

Install DOMShell in your MCP client (Claude Desktop, Cursor, Windsurf, and others).

Repository: https://github.com/apireno/DOMShell

Follow the installation instructions in the repository README, then restart your MCP client.

| | ___|_|___ |___|_|___| | | | | |___|_|___| / | | \ / | | \ |____|_|____| | | | DOMSHELL | | | |___________| |###########| |###########| \#########/ \_______/ ██ ██ ██ ███████ ██ ██ ██ ███ ███████ ██ ██ ██░░░██ ██ ██ ██ ██ ██ ██ ░░ ░░ ░░ ░░ ███████ ██ ██ ███████ ███ ███████ ██░░░░░ ███ ██░░░██ █████ ███ ██ ██ ██░░░ ███ ██ ██ ███████ ░░░ ░░ ░░ ░░░░░░░ ██████ ██████ ███ ███ ██ ██ ██ ██ ██ ████ ████ ██ ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ░░ ██████ ██████ ██ ██ ██ ░░░░░░ ░░░░░░ ░░ ░░ ░░

The browser is your filesystem.A Chrome Extension that lets AI agents (and humans) browse the web using standard Linux commands —ls,cd,cat,grep,click— via a terminal in the Chrome Side Panel.

Install from Chrome Web Store|npm package|Read the blog post|Project home

DOMShell maps the browser into a virtual filesystem. Windows and tabs become top-level directories (~). Each tab's Accessibility Tree becomes a nested filesystem where container elements are directories and buttons, links, and inputs are files. Navigate Chrome the same way you'd navigate/usr/local/bin.

AI agents that interact with websites typically rely on screenshots, pixel coordinates, or brittle CSS selectors. DOMShell takes a different approach: it exposes the browser's own Accessibility Tree as a familiar filesystem metaphor.

- Browsetabs withls ~/tabs/and switch withcd ~/tabs/123instead of guessing which tab is active
- Explorea page withlsandtreeinstead of parsing screenshots
- Navigateinto sections withcd navigation/instead of guessing coordinates
- Acton elements withclick submit_btninstead of fragile DOM queries
- Readcontent withcator bulk-extract withtextinstead of scraping innerHTML
- Searchfor elements withfind --type comboboxinstead of writing selectors

The filesystem abstraction is deterministic, semantic, and works on any website — no site-specific adapters needed.

Install DOMShell directly from theChrome Web Store. No build step required.

git clone https://github.com/apireno/DOMShell.git cd DOMShell npm install npm run build

- Openchrome://extensions/
- EnableDeveloper mode(toggle in top right)
- ClickLoad unpacked
- Select thedist/folder
- Click the DOMShell icon in your toolbar — the side panel opens

Open any webpage, then open the DOMShell side panel. You'll see a terminal:

╔══════════════════════════════════════╗ ║ DOMShell v1.1.0 ║ ║ The browser is your filesystem. ║ ╚══════════════════════════════════════╝ Type 'help' to see available commands. Type 'tabs' to see open browser tabs, then 'cd tabs/<id>' to enter one. dom@shell:~$

You start at~(the browser root). Jump straight to the active tab withhere, or explore:

dom@shell:~$ ls windows/ (2 windows) tabs/ (5 tabs) dom@shell:~$ here ✓ Entered tab 123 Title: Google URL: https://google.com AX Nodes: 247
# List all open tabs dom@shell:~$ tabs ID TITLE URL WIN 123 Google google.com 1 124 GitHub - apireno github.com/apireno 1 125 Wikipedia en.wikipedia.org 2 # Switch to a tab by ID dom@shell:~$ cd tabs/125 ✓ Entered tab 125 Title: Wikipedia URL: https://en.wikipedia.org AX Nodes: 312 # You're now inside the tab's DOM tree dom@shell:~$ pwd ~/tabs/125 # Go back to browser level dom@shell:~$ cd ~ dom@shell:~$ # Or use substring matching dom@shell:~$ cd tabs/github ✓ Entered tab 124 (GitHub - apireno) # List windows (shows tabs grouped under each window) dom@shell:~$ windows Window 1 (focused) ├── 123 Google google.com ├── 124 GitHub - apireno github.com/apireno └── 125 Wikipedia en.wikipedia.org Window 2 ├── 126 Stack Overflow stackoverflow.com └── 127 MDN Web Docs developer.mozilla.org # Browse a specific window's tabs dom@shell:~$ cd windows/2 dom@shell:~/windows/2$ ls ID TITLE URL 125 Wikipedia en.wikipedia.org 126 LinkedIn linkedin.com
# Navigate the current tab to a URL (requires being inside a tab) dom@shell:~$ navigate https://example.com # Open a URL in a new tab (works from anywhere) dom@shell:~$ open https://github.com ✓ Opened new tab URL: https://github.com Title: GitHub AX Nodes: 412

By default DOMShell operates on your general browser —shared mode, exactly as before. Thegroupcommand puts a session in its ownisolated Chrome tab group, so the agent works in a clearly-marked lane while you keep browsing freely in other tabs:

# Create an isolated tab group and work inside it dom@shell:~$ group new research ✓ Created isolated group '🐚 research' [id 4] Working tab: 312 # While isolated, every command is confined to the group's tabs — # entering a tab outside the group is rejected: dom@shell:~$ cd tabs/126 cd: tab 126 is outside the session group (id 4). ... # Show the current mode and group dom@shell:~$ group Group mode: isolated Group: 🐚 research [id 4] Tabs: 1 # Leave the group (it stays open) — back to shared mode dom@shell:~$ group detach # Close the group's DOMShell tabs (your own tabs are kept) dom@shell:~$ group close

Subcommands:group(status),group new [name],group attach <id>,group detach,group close,group list. Isolated mode keeps the agent out of your other tabs; shared mode is the default and unchanged.

When an MCP client connects, DOMShell automatically gives that session its own fresh🐚 agentgroup. The group isleft openwhen the session disconnects (non-destructive) — the agent is instructed to ask whether you'd like it closed before it wraps up, and you can always clear leftovers yourself withgroup close.

Multi-session.Every DOMShell client gets its own session lane — each side-panel window, each MCP connection, separately isolated. Two side panels in two Chrome windows hold independent positions; multiple concurrent MCP agents each work in their own🐚 agentgroup with their own cursor. Rungroup listanytime to see every active lane;group close <id>to close one.

Multiple agents on one MCP connection.Some MCP clients (e.g. Claude Desktop) share one connection across every chat — so by default two chats in the same client would land in one lane. Each chat can carve out its own lane by passing thegroup_idparameter todomshell_execute: pass"new"to create a fresh one (its id is returned at the end of the reply as[lane: <id>]), then pass that id on every later call. Two chats → two lanes → no collision. Agents can also use this forhandoff— one agent reports its lane id, the next agent passes it asgroup_idand continues in the same state. Agents are instructed to close any lane they created when the task is done.

Once you're inside a tab, the Accessibility Tree appears as a filesystem:

# List children of the current node dom@shell:~$ ls navigation/ main/ complementary/ contentinfo/ skip_to_content_link logo_link # Long format shows type prefixes and roles dom@shell:~$ ls -l [d] navigation navigation/ [d] main main/ [x] link skip_to_content_link [x] link logo_link # Filter by type dom@shell:~$ ls --type link skip_to_content_link logo_link # Show DOM metadata (href, src, id) inline — great for finding URLs dom@shell:~$ ls --meta --type link [x] link skip_to_content_link href=https://example.com/#content <a> [x] link logo_link href=https://example.com/ <a> # Paginate large directories dom@shell:~$ ls -n 10 # First 10 items dom@shell:~$ ls -n 10 --offset 10 # Items 11-20 # Count children by type dom@shell:~$ ls --count 45 total (12 [d], 28 [x], 5 [-]) # Enter a directory (container element) dom@shell:~$ cd navigation # See where you are dom@shell:~$ pwd ~/tabs/125/navigation # Go back up dom@shell:~$ cd .. # Jump to browser root dom@shell:~$ cd ~ # Multi-level paths work too dom@shell:~$ cd main/article/form # Path variable: %here% expands to the focused tab (via its window) dom@shell:~$ cd %here% # Enter the active tab dom@shell:~$ cd %here%/.. # Go to the window containing the active tab dom@shell:~$ cd %here%/main # Enter the active tab and cd into main

Every node has a type prefix that communicates metadata without relying on color alone:

# Inspect an element — cat shows full AX + DOM metadata dom@shell:~$ cat submit_btn --- submit_btn --- Role: button Type: [x] interactive AXID: 42 DOM: backend#187 Tag: <button> ID: submit-form Class: btn btn-primary Text: Submit Form HTML: <button id="submit-form" class="btn btn-primary">Submit Form</button> # cat on a link reveals the href URL dom@shell:~$ cat Read_more --- Read_more --- Role: link Type: [x] interactive AXID: 98 DOM: backend#312 Tag: <a> URL: https://en.wikipedia.org/wiki/Article_Title Text: Read more HTML: <a href="https://en.wikipedia.org/wiki/Article_Title">Read more</a> # Navigate to parent to find its properties (e.g. span inside a link) dom@shell:~$ cd .. dom@shell:~$ cat parent_link # Bulk extract ALL text from a section (one call instead of 50+ cat calls) dom@shell:/main$ text [textContent of /main — 4,821 chars] Heading: Welcome to Our Site Today we announce the launch of our new product... (full article text continues) # Extract text from a specific child dom@shell:~$text main [textContent of main — 4,821 chars] # Limit output length dom@shell:~$text main -n 500 # Include link URLs inline as markdown text dom@shell:~$text --links main/article/paragraph_2978 --- Text (with links): paragraph_2978 --- Artificial intelligence (AI) is the capability of computational systems to perform tasks typically associated with human intelligence, such as learning, reasoning... (text + link URLs in a single call) # Get a tree view (default depth: 2) dom@shell:~$tree navigation/ ├── [x] home_link ├── [x] about_link ├── [x] products_link └── [x] contact_link # Deeper tree dom@shell:~$tree 4
# Search current directory dom@shell:~$grep login [x] login_btn (button) [d] login_form (form) [x] login_link (link) # Recursive search across all descendants dom@shell:~$grep -r search [x] search_search (combobox) [x] search_btn (button) # Limit results dom@shell:~$grep -r -n 5 link # Deep search with full paths (like Unix find) dom@shell:~$find search [x] /search_2/search_search (combobox) [x] /search_2/search_btn (button) # Find by role type dom@shell:~$find --type combobox [x] /search_2/search_search (combobox) dom@shell:~$find --type textbox [x] /main/form/email_input (textbox) [x] /main/form/name_input (textbox) # Limit results dom@shell:~$find --type link -n 5 # Find all links with their URLs (great for content extraction) dom@shell:~$find --type link --meta [x] /nav/home_link (link) href=https://example.com/ <a> [x] /main/Read_more (link) href=https://example.com/article <a>

Command Chaining (Bash-Style Composition)

DOMShell works like a filesystem — use the same mental model as searching files on disk.grepdiscovers where content lives (likegrep -rin bash),cdscopes your context, andtext/cat/findreads content (likecat/head/less). The pipe operator (|) filters output, just like bash.

The pattern is: grep (locate) → cd (scope) → extract (read).

# Workflow 1: Find and read an article section dom@shell:~$ grep -r article [d] article (article) → ./main/article/ dom@shell:~$ cd main/article dom@shell:~/main/article$ text [full article content in one call] # Workflow 2: Find a section and extract its links dom@shell:~$ grep -r references [d] references (region) → ./main/article/references/ dom@shell:~$ cd main/article/references dom@shell:~/main/article/references$ find --type link --meta [x] /wiki_link (link) href=https://en.wikipedia.org/... <a> [x] /paper_link (link) href=https://arxiv.org/... <a> # Workflow 3: Find a table and extract structured data dom@shell:~$ grep -r table [d] table_4091 (table) → ./main/section/table_4091/ dom@shell:~$ extract_table table_4091 | Name | Value | Date | |--------|--------|------------| | Alpha | 42 | 2025-01-15 | | Beta | 87 | 2025-02-20 | # Workflow 4: Discover sections, then drill into one dom@shell:~$ grep -r heading [−] Introduction_heading (heading) → ./main/article/Introduction_heading [−] Methods_heading (heading) → ./main/article/Methods_heading [−] Results_heading (heading) → ./main/article/Results_heading dom@shell:~$ cd main/article/Results_heading dom@shell:~/main/article/Results_heading$ text [text content of the Results section] # Workflow 5: Find elements by visible text (not just name) dom@shell:~$ grep -r --content "sign up" [x] get_started_btn (button) → ./main/hero/get_started_btn # The button's NAME is "get_started_btn" but its displayed text says "Sign Up Free" dom@shell:~$ click get_started_btn

The pipe operator (|) lets you filter command output, just like bash:

# Filter find results to only GitHub links dom@shell:~$ find --type link --meta | grep github [x] /main/repo_link (link) href=https://github.com/example <a> # Filter ls output to elements mentioning "login" dom@shell:~$ ls --text | grep login [x] login_btn "Log in to your account" # Limit results with head dom@shell:~$ find --type heading | head -n 3 [−] /main/intro_heading (heading) [−] /main/features_heading (heading) [−] /main/pricing_heading (heading) # Chain multiple pipes dom@shell:~$ find --type link --meta | grep docs | head -n 5

All commands accept relative paths, eliminating the need tocdfirst:

# Read text from a nested element directly dom@shell:~$ text main/article/paragraph_2971 # Click a button inside a form without cd'ing dom@shell:~$ click main/form/submit_btn # Inspect a link in the nav dom@shell:~$ cat navigation/home_link

Use--afterand--beforeflags onlsto find content relative to a landmark:

# Show the 3 elements after a heading dom@shell:~$ ls --after See_also_heading -n 3 --text [d] related_topics_list "Machine Learning, Deep Learning, Neural..." [−] paragraph_4512 "For more information on these topics..." [x] Read_more_link "Read more on Wikipedia" # Find links after a specific section heading dom@shell:~$ ls --after References_heading --type link --meta [x] source_1_link (link) href=https://arxiv.org/... <a> [x] source_2_link (link) href=https://doi.org/... <a>

The key insight:grepoutput feedscd, andcdscopes everything else. When you don't know where content lives on a page, always grep first, then scope, then extract.

# Click a button or link dom@shell:~$click submit_btn ✓ Clicked: submit_btn (button) (tree will auto-refresh on next command) # Focus an input field dom@shell:~$focus email_input ✓ Focused: email_input # Type into the focused field dom@shell:~$type hello@example.com ✓ Typed 17 characters # Navigate to a URL (current tab) dom@shell:~$navigate https://example.com ✓ Navigated to https://example.com # Open a URL in a new tab dom@shell:~$open https://github.com ✓ Opened new tab → https://github.com

DOMShell automatically detects when the page changes — navigation, DOM mutations, or content updates from clicks. You no longer need to manually runrefresh:

dom@shell:~$click search_btn ✓ Clicked: search_btn (button) (tree will auto-refresh on next command) dom@shell:~$ls (page changed — tree refreshed, 312 nodes, path reset to tab root) main/ navigation/ search_results/ ...

If the page navigated, CWD is reset to the tab root. If the DOM just updated in place, your CWD is preserved. You can still force a manual refresh:

dom@shell:~$refresh ✓ Refreshed. 312 AX nodes loaded.

PressTabto auto-complete commands and element names — works like bash:

- Single match: auto-completes inline
- Multiple matches: shows options below, fills the longest common prefix
- cdonly completes directories; other commands complete all elements

Cmd+V (Mac) / Ctrl+V (Windows/Linux) pastes text directly into the terminal. Multi-line pastes are flattened to a single line.

dom@shell:$ ls --help ls — List children of the current node Usage: ls [options] Options: -l, --long Long format: type prefix, role, and name -r, --recursive Show nested children (one level deep) -n N Limit output to first N entries --offset N Skip first N entries (for pagination) --type ROLE Filter by AX role (e.g. --type button) --count Show count of children only ...

DOMShell maps the browser into a two-level virtual filesystem:

The browser itself becomes the top of the filesystem hierarchy:

~ (browser root) ├── windows/ (all Chrome windows) │ ├── <window-id>/ (tabs in that window) │ │ ├── <tab-id> (cd into = enter AX tree) │ │ └── ... │ └── ... └── tabs/ (flat listing of ALL tabs) ├── <tab-id> (cd into = enter AX tree) └── ...

cd-ing into a tab transparently attaches CDP and drops you into its DOM tree.

Each tab'sAccessibility Tree(AXTree) is read via the Chrome DevTools Protocol. Each AX node gets mapped to a virtual file or directory:

Directories(container roles):navigation/,main/,form/,search/,list/,region/,dialog/,menu/,table/,Iframe/, etc.

Files(interactive/leaf roles):submit_btn,home_link,email_input,agree_chk,theme_switch, etc.

cd ..from the DOM root exits back to the tab listing.cd ~returns to browser root from anywhere.

Names are generated from the node's accessible name and role:

Duplicate names are automatically disambiguated with_2,_3, etc.

The AX tree contains many "wrapper" nodes — ignored nodes, unnamed generics, and role=none elements that add structural noise without semantic meaning. DOMShell recursively flattens through these, promoting their children up so you see the meaningful elements without navigating through layers of invisible divs.

DOMShell discovers iframes viaPage.getFrameTreeand fetches each iframe's AX tree separately. Iframe nodes are merged into the main tree with prefixed IDs to avoid collisions, so elements inside iframes appear naturally in the filesystem.

┌────────────────────┐ │ Claude Desktop │──┐ └────────────────────┘ │ ┌────────────────────┐ │ HTTP POST/GET/DELETE ┌─────────────────────┐ │ Claude CLI │──┼─ localhost:3001/mcp ──┐ │ Side Panel (UI) │ └────────────────────┘ │ (Bearer token auth) │ │ │ ┌────────────────────┐ │ │ │ React + Xterm.js │ │ Cursor / Other │──┘ │ │ - Paste support │ └────────────────────┘ ▼ │ - Tab completion │ ┌─────────────────────┐ │ - Command history │ │ MCP Server │ └─────────┬───────────┘ │ (mcp-server/) │ │ │ │ chrome.runtime │ Express HTTP server │ .connect() │ Per-session MCP │ │ │ Security layer: │ ┌─────────▼───────────┐ │ - Auth token │ │ Background Worker │ │ - Command tiers │ │ (Shell Kernel) │ │ - Domain allowlist │ │ │ │ - Audit log │ │ Browser hierarchy │ └──────────┬───────────┘ │ (~, tabs, windows) │ │ │ Command parser │ WebSocket (localhost:9876) │ Shell state (CWD) │ + auth token │ VFS mapper │ + alarm keepalive │ CDP client │ │ │ DOM change detect │ └─────────────────►│ WebSocket bridge │ └─────────┬───────────┘ │ chrome.debugger (CDP 1.3) │ ┌─────────▼───────────┐ │ Active Tab │ │ Accessibility │ │ Tree + iframes │ │ │ │ DOM events ──────►│ │ (auto-refresh) │ └─────────────────────┘

The MCP server runs as astandalone HTTP servicethat any number of MCP clients can connect to simultaneously. It exposes two ports: an HTTP endpoint for MCP clients (default 3001) and a WebSocket bridge for the Chrome extension (default 9876).

The extension follows aThin Client / Fat Hostmodel. The side panel is a dumb terminal — it captures keystrokes, handles paste, and renders ANSI-colored text. All logic lives in the background service worker: command parsing, AX tree traversal, filesystem mapping, CDP interaction, browser hierarchy navigation, and DOM change detection.

src/ background/ index.ts # Shell kernel — commands, state, message router, auto-refresh, WS bridge cdp_client.ts # Promise-wrapped chrome.debugger API + iframe discovery vfs_mapper.ts # Accessibility Tree → virtual filesystem mapping sidepanel/ index.html # Side panel entry HTML index.tsx # React entry point Terminal.tsx # Xterm.js terminal (paste, tab completion, history) shared/ types.ts # Message types, AXNode interfaces, role constants public/ manifest.json # Chrome Manifest V3 options.html # Extension settings page (MCP bridge config) mcp-server/ index.ts # MCP server — standalone Express HTTP + StreamableHTTP, WebSocket bridge, security proxy.ts # Stdio↔HTTP bridge for clients that require command/args (e.g. Claude Desktop) package.json # MCP server dependencies tsconfig.json # MCP server TypeScript config
# Watch mode (rebuilds on file changes) npm run dev # One-time production build npm run build # Type checking npm run typecheck

After building, reload the extension onchrome://extensions/and reopen the side panel to pick up changes.

Connecting MCP Clients (Claude Desktop, CLI, Cursor, etc.)

DOMShell includes a hardened MCP server that lets any MCP-compatible client control the browser through DOMShell commands. The server runs as a standalone HTTP service — multiple clients can connect simultaneously.

DOMShell's MCP server supportsthree install paths— pick whichever matches your setup. Path 1 is the documented default and what most users want. Paths 2 and 3 are optional and exist for users who want container isolation or lifecycle management.

Full Path 2 / Path 3 instructions (build,.envinstall pattern, launchd autostart template, reboot recovery):docs/deploy/container-and-toolhive.md. The rest of this README covers Path 1 — the simplest and recommended default.

npx @apireno/domshell --allow-write --token my-secret-token
User starts independently: npx @apireno/domshell --allow-write --token xyz → HTTP on :3001/mcp (MCP clients) → WebSocket on :9876 (Chrome extension) Claude Desktop spawns (stdio proxy): ┐ npx domshell-proxy --port 3001 --token xyz ├─► HTTP :3001/mcp Claude CLI connects directly: │ url: http://localhost:3001/mcp?token=xyz │ Gemini CLI connects directly: │ url: http://localhost:3001/mcp?token=xyz ┘

The MCP server is astandalone HTTP service— you start it independently, and any number of MCP clients connect to it. No single client "owns" the server process. For clients that require stdio (like Claude Desktop), a tiny proxy bridges stdio to the running HTTP server.

The wizard detects installed MCP clients (Claude Desktop, Cursor, Windsurf), generates a shared token, and writes each client's config. You then start the server once in a terminal — all clients connect to it.

Use--yesfor non-interactive mode with sensible defaults:

npx @apireno/domshell --allow-write --token my-secret-token

- HTTPonhttp://127.0.0.1:3001/mcp— MCP client endpoint
- WebSocketonws://127.0.0.1:9876— Chrome extension bridge

Tip:Use--tokento set a known token so you can pre-configure clients. If omitted, a random token is generated and printed on startup.

Claude CLI / Gemini CLI / Cursor(direct HTTP — recommended):

http://localhost:3001/mcp?token=my-secret-token
No reviews yet — be the first

Sign in to leave a review

Use Google, GitHub, or an email account so ratings stay tied to real people.

Email sign in

No reviews posted yet.