oaustegard/using-webctl
Browser automation via webctl CLI in Claude.ai containers with authenticated proxy support. Use when users mention webctl, browser automation, Playwright browsing, web scraping, or headless Chrome in container environments.
npx skills add https://github.com/oaustegard/claude-skills --skill using-webctl
Browser automation using webctl CLI with automatic proxy authentication handling for Claude.ai's egress-controlled environment.
ERR_TUNNEL_CONNECTION_FAILEDClaude.ai containers route traffic through an authenticated egress proxy (HTTP_PROXY env var with JWT credentials). Chromium doesn't properly handle proxy authentication for HTTPS CONNECT tunnels, causing all HTTPS navigation to fail even though curl works.
A local forwarding proxy (port 18080) intercepts Chromium connections and injects Proxy-Authorization headers before forwarding to the real egress proxy.
pip install webctl --break-system-packages
webctl setup # Downloads Chromium if needed
Copy scripts/auth_proxy.py to webctl's daemon directory:
cp /mnt/skills/user/using-webctl/scripts/auth_proxy.py \
/usr/local/lib/python3.12/dist-packages/webctl/daemon/
Apply this patch to /usr/local/lib/python3.12/dist-packages/webctl/daemon/session_manager.py.
Recent webctl versions split context creation into two branches (mobile emulation vs desktop). Both must receive the proxy config. Find the block (search for cfg = WebctlConfig.load() followed by the if mode == "unattended" and cfg.mobile_emulation: branch — around line 190):
cfg = WebctlConfig.load()
if mode == "unattended" and cfg.mobile_emulation:
# Mobile emulation: cleaner pages, fewer ads, simpler layouts
device = self._playwright.devices["Pixel 7"]
context = await browser.new_context(
storage_state=storage_state,
**device,
)
else:
context = await browser.new_context(
storage_state=storage_state, viewport={"width": 1280, "height": 720}
)
Replace with:
cfg = WebctlConfig.load()
from .auth_proxy import get_local_proxy_url
proxy_url = get_local_proxy_url()
proxy_config = {"server": proxy_url} if proxy_url else None
if mode == "unattended" and cfg.mobile_emulation:
# Mobile emulation: cleaner pages, fewer ads, simpler layouts
device = self._playwright.devices["Pixel 7"]
context = await browser.new_context(
storage_state=storage_state,
proxy=proxy_config,
**device,
)
else:
context = await browser.new_context(
storage_state=storage_state, viewport={"width": 1280, "height": 720},
proxy=proxy_config
)
If the file only has a single new_context call (older webctl), inject proxy=proxy_config into that one and place the three proxy_url lines just above it.
webctl start --mode unattended
webctl --quiet navigate "https://github.com"
webctl snapshot --interactive-only --limit 10
webctl stop
webctl start --mode unattended # Headless browser
webctl stop # Full shutdown (use --keep-daemon to leave daemon running)
webctl status # Current state + console error counts
webctl navigate "https://..."
webctl back / webctl forward / webctl reload
webctl snapshot --interactive-only --limit 30 # Buttons, links, inputs
webctl snapshot --within "role=main" # Scope to container
webctl query "role=button name~=Submit" # Debug queries
webctl screenshot --path shot.png
webctl click 'role=button name~="Submit"'
webctl type 'role=textbox name~="Email"' "[email protected]"
webctl type 'role=textbox name~="Search"' "query" --submit
webctl select 'role=combobox name~="Country"' --label "Germany"
role=button — By ARIA rolename~="partial" — Contains (preferred, more robust)name="exact" — Exact matchnth=0 — Select first when multiple matcheswebctl wait network-idle
webctl wait 'exists:role=button name~="Continue"'
webctl wait 'url-contains:"/dashboard"'
Auth proxy not loaded. Verify:
auth_proxy.py exists in webctl daemon directorywebctl stop && webctl start --mode unattendedAdd specificity or use nth=0:
webctl click 'role=link name="Sign in" nth=0'
netstat -tlnp | grep 18080
Check <network_configuration> in system prompt for allowed domains. Common allowed: *.github.com, *.bsky.app, allowed API endpoints.
Reduce context consumption:
webctl snapshot --interactive-only --limit 30 # Cap elements
webctl snapshot | grep -i "submit" # Unix filtering
webctl --quiet navigate "..." # Suppress events
Take oaustegard/using-webctl from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.
The instructions reference pip.
Without those the skill loads but fails at the first command.