Browser automation via CLI — for humans and agents
Project description
webctl
Browser automation for AI agents and humans, built on the command line.
# 1. Install
pip install webctl
# 2. Auto-configure your agent (creates skills/prompts for all supported agents)
webctl init
# 3. Start browsing
webctl start
webctl navigate "https://google.com"
webctl snapshot --interactive-only
webctl init automatically generates the skills and prompts your agents need to drive the browser.
Why CLI Instead of MCP?
MCP browser tools have a fundamental problem: the server controls what enters your context. With Playwright MCP, every response includes the full accessibility tree plus console messages. After a few page queries, your context window is full.
CLI flips this around: you control what enters context.
# Filter before context
webctl snapshot --interactive-only --limit 30 # Only buttons, links, inputs
webctl snapshot --within "role=main" # Skip nav, footer, ads
# Pipe through Unix tools
webctl snapshot | grep -i "submit" # Find specific elements
webctl --format jsonl snapshot | jq '.data.role' # Extract with jq
Beyond filtering, CLI gives you:
| Capability | CLI | MCP |
|---|---|---|
| Filter output | Built-in flags + grep/jq/head | Server decides |
| Debug | Run same command as agent | Opaque |
| Cache & Cost | webctl snapshot > cache.txt |
Every call hits server |
| Script | Save to .sh, version control | Ephemeral |
| Human takeover | Same commands | Different interface |
Agent Integration
Step 1: Install
pip install webctl
webctl setup # Downloads Chromium
Step 2: Generate Skills/Prompts
webctl init # Project-level (recommended)
webctl init --global # Global (works across all projects)
This creates:
- Skills for Claude Code and Goose (loaded on-demand when doing web tasks)
- Lean prompts for Gemini, Copilot, and Codex (always in context)
Supported agents:
| Agent | Format | Location (project) | Location (global) |
|---|---|---|---|
claude |
Skill | .claude/skills/webctl/SKILL.md |
~/.claude/skills/webctl/SKILL.md |
goose |
Skill | .agents/skills/webctl/SKILL.md |
~/.config/agents/skills/webctl/SKILL.md |
gemini |
Prompt | GEMINI.md |
~/.gemini/GEMINI.md |
copilot |
Prompt | .github/copilot-instructions.md |
- |
codex |
Prompt | AGENTS.md |
~/.codex/AGENTS.md |
claude-noskill |
Prompt | CLAUDE.md (legacy) |
~/.claude/CLAUDE.md |
Why skills? Skills are loaded on-demand - your agent only reads webctl instructions when actually doing web automation. This keeps your context clean for other tasks.
Select specific agents:
webctl init --agents claude,gemini # Only Claude and Gemini
webctl init --agents claude-noskill # Legacy CLAUDE.md format
Step 3: Add to Config (optional)
If your agent doesn't auto-detect the generated files, add this to your system prompt:
For web browsing, use webctl CLI. Run
webctl agent-promptfor instructions.
Note: If a browser MCP is already configured, disable it to avoid conflicts.
Quick Start (Human Usage)
Verify the installation works by driving it yourself:
webctl start # Opens visible browser window
webctl navigate "https://example.com"
webctl snapshot --interactive-only
webctl stop --daemon # Closes browser and daemon
Global installation with `uv`
uv tool install webctl
uv tool run webctl
Linux system dependencies
playwright install-deps chromium
# Or manually install libraries listed in Playwright documentation
Core Concepts
Sessions
Browser stays open across commands. Cookies persist to disk.
webctl start # Visible browser
webctl start --mode unattended # Headless (invisible)
webctl -s work start # Named profile (separate cookies)
Element Queries
Semantic targeting based on ARIA roles - stable across CSS refactors:
role=button # Any button
role=button name="Submit" # Exact match
role=button name~="Submit" # Contains text (preferred)
Output Control
webctl snapshot # Human-readable
webctl --quiet navigate "..." # Suppress events
webctl --result-only --format jsonl navigate "..." # Pure JSON
Commands
Navigation & Observation
webctl navigate "https://..."
webctl back / forward / reload
webctl snapshot --interactive-only # Buttons, links, inputs only
webctl snapshot --within "role=main" # Scope to container
webctl query "role=button name~=Submit" # Debug query
webctl screenshot --path shot.png
Interaction
webctl click 'role=button name~="Submit"'
webctl type 'role=textbox name~="Email"' "user@example.com"
webctl type 'role=textbox name~="Search"' "query" --submit
webctl select 'role=combobox name~="Country"' --label "Germany"
webctl check 'role=checkbox name~="Remember"'
webctl press Enter
webctl scroll down
Wait Conditions
webctl wait network-idle
webctl wait 'exists:role=button name~="Continue"'
webctl wait 'url-contains:"/dashboard"'
Session & Console
webctl status # Current state & error counts
webctl save # Persist cookies now
webctl console --count # Just counts by level (LLM-friendly)
webctl console --level error # Filter to errors only
Architecture
┌─────────────┐ Unix Socket ┌─────────────┐
│ CLI │ ◄────────────► │ Daemon │
│ (webctl) │ JSON-RPC │ (browser) │
└─────────────┘ └─────────────┘
│ │
▼ ▼
Agent/User Chromium + Playwright
- CLI: Stateless, sends commands to daemon
- Daemon: Manages browser, auto-starts on first command
- Socket:
$WEBCTL_SOCKET_DIRor OS default (see below) - Profiles:
~/.local/share/webctl/profiles/
Socket Paths
| Platform | Default |
|---|---|
| Linux | /run/user/<uid>/webctl-<session>.sock |
| macOS | /tmp/webctl-<session>.sock |
| Windows | %TEMP%\webctl-<session>.sock |
Override directory with WEBCTL_SOCKET_DIR environment variable.
Container Deployment
Set WEBCTL_SOCKET_DIR to share the Unix socket between host and container (or between containers).
Daemon in Container, Client on Host
mkdir -p /tmp/webctl-ipc
docker run -d --name webctl-daemon \
-u $(id -u):$(id -g) \
-v /tmp/webctl-ipc:/ipc \
-e WEBCTL_SOCKET_DIR=/ipc \
my-webctl-image python -m webctl.daemon.server
export WEBCTL_SOCKET_DIR=/tmp/webctl-ipc
webctl start && webctl navigate "https://example.com"
-u $(id -u):$(id -g) ensures the socket file is owned by your host user.
Daemon and Client in Separate Containers
docker volume create webctl-ipc
docker run -d --name webctl-daemon \
-v webctl-ipc:/ipc \
-e WEBCTL_SOCKET_DIR=/ipc \
my-webctl-image python -m webctl.daemon.server
docker run --rm \
-v webctl-ipc:/ipc \
-e WEBCTL_SOCKET_DIR=/ipc \
my-webctl-image webctl navigate "https://example.com"
No UID matching needed - both containers run as the same user.
Security
IPC Authentication
webctl verifies that CLI commands come from the same user as the daemon:
| Platform | Mechanism | Strength |
|---|---|---|
| Linux | SO_PEERCRED |
Kernel-enforced UID check |
| macOS | LOCAL_PEERCRED |
Kernel-enforced UID check |
| Windows | SIO_AF_UNIX_GETPEERPID + process token |
Kernel-enforced SID check |
All platforms use kernel-level credential verification. This prevents other users from controlling your browser session.
Note: Root/Administrator can still access any user's session (OS limitation).
License
MIT
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file webctl-0.3.0.tar.gz.
File metadata
- Download URL: webctl-0.3.0.tar.gz
- Upload date:
- Size: 116.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
60c0a575e901525a1562b96017c12497b1954a240aca3ef21e9ce886b2b70272
|
|
| MD5 |
d6ad582c7e1ea67b8d5195674adc12db
|
|
| BLAKE2b-256 |
44a2ae6095ff0a7eedd2fa47cc70ab4c8fb0ad182ce0ed9c266b261c5d687cfe
|
Provenance
The following attestation bundles were made for webctl-0.3.0.tar.gz:
Publisher:
publish.yml on cosinusalpha/webctl
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
webctl-0.3.0.tar.gz -
Subject digest:
60c0a575e901525a1562b96017c12497b1954a240aca3ef21e9ce886b2b70272 - Sigstore transparency entry: 846215873
- Sigstore integration time:
-
Permalink:
cosinusalpha/webctl@5f6028a556584726a4519bfd578031743cd91280 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/cosinusalpha
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@5f6028a556584726a4519bfd578031743cd91280 -
Trigger Event:
push
-
Statement type:
File details
Details for the file webctl-0.3.0-py3-none-any.whl.
File metadata
- Download URL: webctl-0.3.0-py3-none-any.whl
- Upload date:
- Size: 89.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
02151347bd32b598296967af007467d35b89736b286f2a77503a6e73401384ba
|
|
| MD5 |
933fe5ca18d5ef94f40455534d26a775
|
|
| BLAKE2b-256 |
bf170d842a57e2edecab98b6d00429e07042e62147b0c362be64a2551d7f31fc
|
Provenance
The following attestation bundles were made for webctl-0.3.0-py3-none-any.whl:
Publisher:
publish.yml on cosinusalpha/webctl
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
webctl-0.3.0-py3-none-any.whl -
Subject digest:
02151347bd32b598296967af007467d35b89736b286f2a77503a6e73401384ba - Sigstore transparency entry: 846215876
- Sigstore integration time:
-
Permalink:
cosinusalpha/webctl@5f6028a556584726a4519bfd578031743cd91280 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/cosinusalpha
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@5f6028a556584726a4519bfd578031743cd91280 -
Trigger Event:
push
-
Statement type: