The DevTools console, from your terminal and editor.
Evaluate JavaScript in any browser tab from the command line. Everything runs via CDP Runtime.evaluate — the same mechanism as the DevTools console. This bypasses Content-Security-Policy on any page. Globals persist across calls. No script tags, no CORS issues.
Named script injection with auto-reload. Register scripts in .cjig.json, inject by name, watch files for changes, auto re-inject. The modify → re-inject → exercise loop without leaving your editor.
Chrome lifecycle management. Launch Chrome with isolated profiles, load unpacked extensions, attach to running instances. cjig connection-info exports connection details so Playwright scripts (or MCP browser tools) can connect to the same Chrome.
Editor-native via nREPL. Evaluate ClojureScript from Neovim/Conjure buffers directly in the browser. No browser tab switching, no copy-paste.
Independent developer workflow. The CLI is usable without any LLM. cjig launch && cjig inject my-script && cjig repl is a complete development loop with no AI in the path.
# From npm registry
pnpm add -g chrome-jig
# Or for development
git clone https://github.com/yourname/chrome-jig.git
cd chrome-jig
pnpm install
npm link # uses nvm's bin directory# Launch Chrome with debugging enabled
cjig launch
# Evaluate JavaScript
cjig eval "document.title"
# Target a specific tab
cjig eval --tab "GitHub" "document.title"
# Evaluate a file (bypasses CSP on any page)
cjig eval-file bundle.js
# Start interactive REPL
cjig replcjig and MCP browser tools complement each other — both connect to Chrome via CDP on the same port.
cjig manages Chrome, MCP provides automation:
# cjig launches Chrome with extensions and a debug port
cjig launch --extensions ./my-extension/dist
# Playwright MCP connects to the same Chrome
# In your MCP client config:{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest", "--cdp-endpoint", "http://localhost:9222"]
}
}
}If MCP already launched Chrome, attach to it:
cjig attach --port 9222
cjig eval "document.title" # Now cjig commands work against MCP's ChromeExport connection info for scripts:
cjig connection-info --json
# {"host":"localhost","port":9222,"endpoint":"http://localhost:9222","webSocketDebuggerUrl":"ws://...","source":"launched","profile":"default"}All evaluation uses CDP Runtime.evaluate in the page's main world — the same context as the DevTools console:
cjig evalevaluates an expression via CDP. Bypasses CSP.cjig injectfetches the script URL server-side (in the Node.js process), then evaluates the content via CDP. Bypasses both CSP and CORS.cjig eval-filereads a local file and evaluates its contents via CDP. Bypasses CSP.
Each CLI invocation is a fresh process. The remembered default tab does persist between invocations through session state: cjig tab <selector> sets the default tab for later CLI commands, while --tab stays a one-shot override. Use cjig repl for a fully persistent interactive session.
cjig launch # Launch with default profile
cjig launch --profile=testing # Named profile
cjig launch --extensions /path/to/ext # Load unpacked extension
cjig attach --port 9333 # Attach to running Chrome
cjig status # Check if Chrome is running
cjig connection-info # Show connection details
cjig connection-info --json # JSON output for scriptscjig tabs # List open tabs (index + title + URL)
cjig tab "GitHub" # Select + remember by title or URL fragment
cjig tab 2 # Select + remember by index
cjig open https://example.com # Open new tab
cjig open --timeout 60000 https://heavy-page.com # Custom timeout
cjig open --wait-until domcontentloaded https://example.com
cjig open --no-wait https://slow-page.com # Fire-and-forgetTab selector: numbers are positional indices, strings search URL and title.
cjig inject my-script # Inject by name (from config)
cjig inject --tab "app" my-script # Inject into specific tab
cjig inject https://... # Inject by URLcjig eval "document.title" # One-shot eval
cjig eval --tab "GitHub" "document.title" # Eval in specific tab
cjig eval "window.myApi.status()" # Call injected API
cjig eval-file bundle.js # Evaluate a file
cjig eval-file --tab 2 bundle.js # File eval in specific tab
cat script.js | cjig eval-file - # Pipe from stdin
cjig cljs-eval "(+ 1 2)" # Evaluate ClojureScript
cjig repl # Interactive REPLcjig nrepl # Start server, auto-assign port
cjig nrepl --nrepl-port 7888 # Specific portStarts a TCP nREPL server for native editor integration. ClojureScript forms are compiled via squint and evaluated in the browser over CDP.
Editors discover the port via .nrepl-port written to the current directory.
- Conjure (Neovim): Connects automatically. Evaluate CLJS forms in your buffer with standard Conjure keybindings.
- CIDER (Emacs): Not yet supported — CIDER's handshake expects richer metadata than we currently provide.
The REPL and nREPL share a single connection. Tab switches in the REPL (.tab) take effect for nREPL evaluations too — no reconnection needed.
cjig profiles list # List known profiles
cjig profiles create myext --extensions /path/ext # Create profile with extensions
cjig launch --profile=myext # Launch with profile configProfile configs live at ~/.config/cjig/profiles/<name>.json and remember extensions, flags, and default URL. Login sessions persist across launches in the profile's Chrome user-data directory.
# Via CLI flag
cjig launch --extensions /path/to/unpacked-extension
# Via project config (.cjig.json)
{
"extensions": ["/path/to/unpacked-extension"]
}
# Via profile config
cjig profiles create dev --extensions /path/to/ext
cjig launch --profile=devExtensions from CLI flags, project config, profile config, and global config are merged (deduplicated by path).
import { getConnectionInfo } from 'chrome-jig';
import { chromium } from 'playwright';
const { info } = await getConnectionInfo('localhost', 9222);
const browser = await chromium.connectOverCDP(info.endpoint);
// Now use Playwright's full API against cjig-managed Chrome> expression Evaluate JavaScript in browser
.help Show available commands
.tabs List open tabs
.tab <pattern|index> Switch to tab
.open <url> Open new tab
.inject <name|url> Inject script
.reload Reload current tab
.watch [on|off] Toggle file watching
.build Run preBuild hook
.config Show current config
.clear Clear console
.exit Exit REPL
{
"defaults": {
"port": 9222,
"profile": "default"
},
"chrome": {
"path": "/path/to/chrome",
"flags": ["--disable-background-timer-throttling"]
},
"extensions": ["/path/to/global-extension"],
"connection": {
"retries": 3,
"retryDelayMs": 500,
"fallbackHosts": ["127.0.0.1"]
}
}{
"scripts": {
"baseUrl": "http://localhost:5173/harnesses/",
"registry": {
"bs": {
"path": "block-segmenter-harness.js",
"label": "Block Segmenter",
"windowApi": "BlockSegmenter",
"alias": "BS",
"quickStart": "BS.overlayOn()"
}
}
},
"extensions": ["/path/to/project-extension"],
"watch": {
"paths": ["dist/harnesses/*.js"],
"debounce": 300
},
"hooks": {
"preBuild": "pnpm build:harnesses"
}
}{
"extensions": ["/path/to/extension"],
"flags": ["--auto-open-devtools-for-tabs"],
"url": "http://localhost:3000"
}Extension merge priority: CLI flags > project config > profile config > global config.
Connection resilience settings can be configured at any level (global, project, or CLI flags):
{
"connection": {
"retries": 3,
"retryDelayMs": 500,
"timeout": 30000,
"waitUntil": "domcontentloaded",
"fallbackHosts": ["127.0.0.1"]
}
}| Field | Default | Description |
|---|---|---|
retries |
3 |
Number of connection retry attempts |
retryDelayMs |
500 |
Initial delay between retries (doubles each attempt) |
timeout |
(Playwright default) | Navigation timeout in ms for open |
waitUntil |
load |
Navigation strategy: load, domcontentloaded, networkidle |
fallbackHosts |
[] |
Additional hosts to try on connect failure (e.g. ["127.0.0.1"] for IPv6/IPv4 issues) |
CLI flags --retries, --retry-delay, --timeout, --wait-until, and --no-wait override config values.
cjig uses typed exit codes for machine-parseable error handling:
| Exit Code | Category | Retryable | Meaning |
|---|---|---|---|
| 0 | — | — | Success |
| 1 | — | — | Unknown error |
| 2 | connection |
yes | Cannot connect to Chrome |
| 3 | timeout |
yes | Navigation timed out |
| 4 | no-page |
no | No page/tab available |
| 5 | evaluation |
no | JavaScript evaluation error |
With --json, errors are emitted as structured JSON on stderr:
cjig eval --json --port 9999 "1+1"
# stderr: {"error":"Failed to connect...","category":"connection","retryable":true,"exitCode":2}| Variable | Default | Description |
|---|---|---|
CJIG_PORT |
9222 |
CDP port |
CJIG_PROFILE |
default |
Profile name |
CJIG_HOST |
localhost |
Chrome host |
CHROME_PATH |
(auto-detect) | Chrome executable |
cjig env >> ~/.zshrc
source ~/.zshrcThis adds:
cjr- alias forcjig replcjl- alias forcjig launchcjt- alias forcjig tabs
cjig install-skill # Symlinks this package to ~/.claude/skills/chrome-jig
cjig uninstall-skill # Removes the symlinkThen Claude can use it via the SKILL.md instructions.
~/.config/cjig/
├── config.json # Global config
└── profiles/ # Named profile configs
└── myext.json
~/.local/share/cjig/
└── chrome-profiles/ # Chrome user-data dirs
├── default/
└── myext/
~/.local/state/cjig/
└── last-session.json # Session state
See ARCHITECTURE.md for technical internals — module structure, data flow diagrams, CDP execution model, and design decisions.
MIT