Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 21 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Chrome DevTools CLI

High-performance rust CLI that connects to an existing Chrome browser via the DevTools Protocol. Auto-connects by default, no manual WebSocket URL needed.
High-performance rust CLI that connects to an existing Chrome or Edge browser via the DevTools Protocol. Auto-connects by default, no manual WebSocket URL needed.

[![crates.io](https://img.shields.io/crates/v/chrome-devtools-cli.svg)](https://crates.io/crates/chrome-devtools-cli)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
Expand Down Expand Up @@ -42,12 +42,12 @@ This is a lightweight Rust binary that talks directly to Chrome's DevTools Proto
```
chrome-devtools navigate https://example.com
├─ Try daemon (Unix socket /tmp/chrome-devtools-daemon.sock)
├─ Try daemon (Unix socket on macOS/Linux, TCP localhost on Windows)
│ └─ If running → send command → get result
├─ If no daemon → spawn one (background process)
│ └─ Daemon connects to Chrome WebSocket (one-time approval)
│ └─ Listens on Unix socket, 5-min idle timeout
│ └─ Daemon connects to Chrome/Edge WebSocket (one-time approval)
│ └─ Listens for IPC connections, 5-min idle timeout
└─ Fallback → direct WebSocket connection (no daemon)
```
Expand All @@ -56,23 +56,23 @@ The daemon keeps a persistent WebSocket connection to Chrome, so the browser onl

## Prerequisites

Chrome must have remote debugging enabled:
Chrome or Edge must have remote debugging enabled:

1. Open Chrome
2. Go to `chrome://inspect/#remote-debugging`
1. Open Chrome/Edge
2. Go to `chrome://inspect/#remote-debugging` (or `edge://inspect/#remote-debugging`)
3. Enable the remote debugging server

## Auto-connect

By default, the CLI reads `DevToolsActivePort` from Chrome's user data directory:
By default, the CLI reads `DevToolsActivePort` from the browser's user data directory:

| OS | Default path |
|----|-------------|
| macOS | `~/Library/Application Support/Google/Chrome/` |
| Linux | `~/.config/google-chrome/` |
| Windows | `%LOCALAPPDATA%\Google\Chrome\User Data\` |
| OS | Chrome (default) | Edge (`--channel edge`) |
|----|-----------------|------------------------|
| macOS | `~/Library/Application Support/Google/Chrome/` | `~/Library/Application Support/Microsoft Edge/` |
| Linux | `~/.config/google-chrome/` | `~/.config/microsoft-edge/` |
| Windows | `%LOCALAPPDATA%\Google\Chrome\User Data\` | `%LOCALAPPDATA%\Microsoft\Edge\User Data\` |

Override with `--user-data-dir`, `--channel` (beta/canary/dev), or `--ws-endpoint`. All three also read from environment variables:
Override with `--user-data-dir`, `--channel` (stable/beta/canary/dev/edge/edge-beta/edge-canary/edge-dev), or `--ws-endpoint`. All three also read from environment variables:

| Environment Variable | Corresponding Flag |
|----------------------|--------------------|
Expand Down Expand Up @@ -174,16 +174,17 @@ These commands interact with tools injected into the page via `window.__dtmcp.to
| `--json` | JSON output |
| `--ws-endpoint <url>` | Explicit WebSocket URL |
| `--user-data-dir <path>` | Custom Chrome profile directory |
| `--channel <ch>` | Chrome channel (stable/beta/canary/dev) |
| `--channel <ch>` | Browser channel (stable/beta/canary/dev/edge/edge-beta/edge-canary/edge-dev) |

## Daemon details

- **Socket**: `/tmp/chrome-devtools-daemon.sock`
- **PID file**: `/tmp/chrome-devtools-daemon.pid`
- **Idle timeout**: 5 minutes (auto-exits, cleans up socket)
- **Protocol**: Length-prefixed JSON over Unix socket
- **IPC (macOS/Linux)**: Unix socket at `/tmp/chrome-devtools-daemon.sock`
- **IPC (Windows)**: TCP on `127.0.0.1` with address stored in `%TEMP%\chrome-devtools-daemon.addr`
- **PID file**: `/tmp/chrome-devtools-daemon.pid` (Unix) or `%TEMP%\chrome-devtools-daemon.pid` (Windows)
Comment on lines +181 to +183

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Daemon path docs should not hardcode /tmp on Unix.

The implementation uses std::env::temp_dir() (src/protocol.rs), so Unix paths are not guaranteed to be /tmp/.... Please document these as system temp-dir paths to avoid debugging confusion.

Suggested doc patch
-- **IPC (macOS/Linux)**: Unix socket at `/tmp/chrome-devtools-daemon.sock`
-- **PID file**: `/tmp/chrome-devtools-daemon.pid` (Unix) or `%TEMP%\chrome-devtools-daemon.pid` (Windows)
+- **IPC (macOS/Linux)**: Unix socket in the system temp directory (e.g. `/tmp/chrome-devtools-daemon.sock`)
+- **PID file**: In the system temp directory (e.g. `/tmp/chrome-devtools-daemon.pid` on many Unix systems) or `%TEMP%\chrome-devtools-daemon.pid` (Windows)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- **IPC (macOS/Linux)**: Unix socket at `/tmp/chrome-devtools-daemon.sock`
- **IPC (Windows)**: TCP on `127.0.0.1` with address stored in `%TEMP%\chrome-devtools-daemon.addr`
- **PID file**: `/tmp/chrome-devtools-daemon.pid` (Unix) or `%TEMP%\chrome-devtools-daemon.pid` (Windows)
- **IPC (macOS/Linux)**: Unix socket in the system temp directory (e.g. `/tmp/chrome-devtools-daemon.sock`)
- **IPC (Windows)**: TCP on `127.0.0.1` with address stored in `%TEMP%\chrome-devtools-daemon.addr`
- **PID file**: In the system temp directory (e.g. `/tmp/chrome-devtools-daemon.pid` on many Unix systems) or `%TEMP%\chrome-devtools-daemon.pid` (Windows)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 181 - 183, Update the README to stop hardcoding
`/tmp` for Unix temp paths and instead state that IPC socket and PID file are
placed in the system temporary directory (the implementation uses
std::env::temp_dir() in src/protocol.rs), e.g. "system temp directory (returned
by std::env::temp_dir())" for Unix, while keeping the Windows %TEMP% references;
adjust the two bullets for IPC (macOS/Linux) and PID file (Unix) to reflect
"system temp-dir" rather than `/tmp`.

- **Idle timeout**: 5 minutes (auto-exits, cleans up)
- **Protocol**: Length-prefixed JSON
- **Spawned by**: First CLI invocation (transparent to user)
- **Kill manually**: `pkill -f __daemon__` or delete the socket
- **Kill manually**: `pkill -f __daemon__` (Unix) or terminate via PID file (Windows)

## Source layout

Expand Down
15 changes: 10 additions & 5 deletions skill/chrome-devtools/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
---
name: chrome-devtools
description: Use when the user asks to "take a screenshot of a website", "navigate to a URL", "fill a form in the browser", "interact with Chrome", or when a chrome automation task is needed.
description: Use when the user asks to "take a screenshot of a website", "navigate to a URL", "fill a form in the browser", "interact with Chrome/Edge", "connect Chrome/Edge", or when a browser automation task is needed.
user-invocable: true
---

## Prerequisites

Chrome must have remote debugging enabled:
1. Open Chrome
2. Go to `chrome://inspect/#remote-debugging`
Chrome or Edge must have remote debugging enabled:
1. Open Chrome/Edge
2. Go to `chrome://inspect/#remote-debugging` (or `edge://inspect/#remote-debugging`)
3. Enable the remote debugging server

The CLI auto-connects by reading Chrome's `DevToolsActivePort` file — no WebSocket URL needed.
The CLI auto-connects by reading the browser's `DevToolsActivePort` file — no WebSocket URL needed.

For Edge, pass `--channel edge`:
```bash
chrome-devtools --channel edge list-pages
```

## Core Capabilities

Expand Down
49 changes: 30 additions & 19 deletions src/browser.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use anyhow::{anyhow, bail, Result};
use std::path::{Path, PathBuf};

/// Resolve the WebSocket URL for connecting to Chrome.
/// Resolve the WebSocket URL for connecting to Chrome or Edge.
///
/// Priority:
/// 1. Explicit --ws-endpoint
Expand Down Expand Up @@ -31,9 +31,9 @@ fn read_devtools_active_port(user_data_dir: &Path) -> Result<String> {
let content = std::fs::read_to_string(&port_path).map_err(|_| {
anyhow!(
"Could not read DevToolsActivePort at {}\n\n\
Make sure Chrome is running with remote debugging enabled:\n\
1. Open Chrome\n\
2. Go to chrome://inspect/#remote-debugging\n\
Make sure Chrome or Edge is running with remote debugging enabled:\n\
1. Open Chrome/Edge\n\
2. Go to chrome://inspect/#remote-debugging (or edge://inspect/#remote-debugging)\n\
3. Enable the remote debugging server",
port_path.display()
)
Expand All @@ -57,25 +57,28 @@ fn read_devtools_active_port(user_data_dir: &Path) -> Result<String> {
.map_err(|_| anyhow!("Invalid port '{}' in DevToolsActivePort", lines[0]))?;

if port == 0 {
bail!("Port 0 in DevToolsActivePort — Chrome may not be running");
bail!("Port 0 in DevToolsActivePort — browser may not be running");
}

let path = lines[1];
Ok(format!("ws://127.0.0.1:{port}{path}"))
}

/// Get the default Chrome user data directory for the given channel.
/// Get the default browser user data directory for the given channel.
fn default_chrome_user_data_dir(channel: &str) -> Result<PathBuf> {
#[cfg(target_os = "macos")]
{
let home = dirs::home_dir().ok_or_else(|| anyhow!("Cannot determine home directory"))?;
let base = home.join("Library/Application Support/Google");
let dir = match channel {
Comment on lines 68 to 72

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The channel parameter is matched against exact lowercase string literals (e.g., "edge", "stable"). If a user passes a channel with different casing (e.g., --channel Edge or --channel Stable), the match will fail with an Unknown browser channel error. Converting the channel string to lowercase before matching makes the CLI more robust and user-friendly.

Suggested change
fn default_chrome_user_data_dir(channel: &str) -> Result<PathBuf> {
#[cfg(target_os = "macos")]
{
let home = dirs::home_dir().ok_or_else(|| anyhow!("Cannot determine home directory"))?;
let base = home.join("Library/Application Support/Google");
let dir = match channel {
fn default_chrome_user_data_dir(channel: &str) -> Result<PathBuf> {
let channel_lower = channel.to_lowercase();
let channel = channel_lower.as_str();
#[cfg(target_os = "macos")]
{
let home = dirs::home_dir().ok_or_else(|| anyhow!("Cannot determine home directory"))?;
let dir = match channel {

"stable" | "chrome" => base.join("Chrome"),
"beta" => base.join("Chrome Beta"),
"canary" => base.join("Chrome Canary"),
"dev" => base.join("Chrome Dev"),
_ => bail!("Unknown Chrome channel: {channel}"),
"stable" | "chrome" => home.join("Library/Application Support/Google/Chrome"),
"beta" => home.join("Library/Application Support/Google/Chrome Beta"),
"canary" => home.join("Library/Application Support/Google/Chrome Canary"),
"dev" => home.join("Library/Application Support/Google/Chrome Dev"),
"edge" => home.join("Library/Application Support/Microsoft Edge"),
"edge-beta" => home.join("Library/Application Support/Microsoft Edge Beta"),
"edge-canary" => home.join("Library/Application Support/Microsoft Edge Canary"),
"edge-dev" => home.join("Library/Application Support/Microsoft Edge Dev"),
_ => bail!("Unknown browser channel: {channel}. Valid: stable, beta, canary, dev, edge, edge-beta, edge-canary, edge-dev"),
};
Ok(dir)
}
Expand All @@ -88,7 +91,11 @@ fn default_chrome_user_data_dir(channel: &str) -> Result<PathBuf> {
"beta" => home.join(".config/google-chrome-beta"),
"canary" => home.join(".config/google-chrome-unstable"),
"dev" => home.join(".config/google-chrome-unstable"),
_ => bail!("Unknown Chrome channel: {channel}"),
"edge" => home.join(".config/microsoft-edge"),
"edge-beta" => home.join(".config/microsoft-edge-beta"),
"edge-canary" => home.join(".config/microsoft-edge-canary"),
"edge-dev" => home.join(".config/microsoft-edge-dev"),
_ => bail!("Unknown browser channel: {channel}. Valid: stable, beta, canary, dev, edge, edge-beta, edge-canary, edge-dev"),
};
Ok(dir)
}
Expand All @@ -97,13 +104,17 @@ fn default_chrome_user_data_dir(channel: &str) -> Result<PathBuf> {
{
let local_app_data =
std::env::var("LOCALAPPDATA").map_err(|_| anyhow!("LOCALAPPDATA not set"))?;
let base = PathBuf::from(local_app_data).join("Google");
let base = PathBuf::from(&local_app_data);
let dir = match channel {
"stable" | "chrome" => base.join("Chrome/User Data"),
"beta" => base.join("Chrome Beta/User Data"),
"canary" => base.join("Chrome SxS/User Data"),
"dev" => base.join("Chrome Dev/User Data"),
_ => bail!("Unknown Chrome channel: {channel}"),
"stable" | "chrome" => base.join("Google/Chrome/User Data"),
"beta" => base.join("Google/Chrome Beta/User Data"),
"canary" => base.join("Google/Chrome SxS/User Data"),
"dev" => base.join("Google/Chrome Dev/User Data"),
"edge" => base.join("Microsoft/Edge/User Data"),
"edge-beta" => base.join("Microsoft/Edge Beta/User Data"),
"edge-canary" => base.join("Microsoft/Edge SxS/User Data"),
"edge-dev" => base.join("Microsoft/Edge Dev/User Data"),
_ => bail!("Unknown browser channel: {channel}. Valid: stable, beta, canary, dev, edge, edge-beta, edge-canary, edge-dev"),
};
Ok(dir)
}
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ pub struct Cli {
#[arg(long, global = true, env = "CHROME_USER_DATA_DIR")]
pub user_data_dir: Option<String>,

/// Chrome channel: stable, beta, canary, dev
/// Browser channel: stable, beta, canary, dev, edge, edge-beta, edge-canary, edge-dev
#[arg(long, global = true, default_value = "stable", env = "CHROME_CHANNEL")]
pub channel: String,

Expand Down