You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
An agent (e.g. Claude Code) running inside a deacon-managed devcontainer cannot drive a Chrome instance running on the developer's host desktop — there's no network path from the container to a host process, and no standard way to tell an in-container MCP client where to find it. This blocks browser-automation workflows (visual debugging, screenshot loops, page-navigation QA) that assume the agent can reach chrome-devtools-mcp pointed at the host browser's --remote-debugging-port.
Note on the original ask: the request specifies deacon shell --browser. That subcommand doesn't exist — crates/deacon/src/cli.rs:245's Commands enum lists only Up, Build, Exec, ReadConfiguration, Config, Templates, Upgrade, SetUp, RunUserCommands, Down, Doctor, Outdated, ForwardDaemon. This issue reframes the flag as living on deacon up (and/or deacon exec), consistent with the real subcommand surface.
Two problems bundled together — recommend splitting scope
The request bundles (1) a network-reachability problem and (2) an app-specific config-injection problem. These have very different risk profiles and should likely be split:
1. Container→host reachability (new plumbing, needs its own design)
This is the reverse direction of everything deacon currently does for port handling. The existing 015-auto-forward-ports daemon (crates/core/src/port_forward/{daemon.rs,relay.rs,registry.rs}) solves host→container reachability: the host binds a loopback listener and, per connection, runs docker exec -i <id> nc/socat ... to dial into the container's netns (relay.rs:1-6,56, the relay program dials {dial_host}:{container_port} from inside the container, driven by a host-side listener). There is currently zero code path for the opposite direction — confirmed via grep: no hits for host.docker.internal, host-gateway, add-host, or extra_hosts/ExtraHosts anywhere in docker.rs, compose.rs, container.rs, or up/*. Making a container process reach a host port is genuinely new work, not an extension of the existing daemon's direction.
This same container→host reachability gap is also the crux of a related open issue (host clipboard bridge, #281) — worth designing once and sharing the underlying mechanism (e.g. a generic "reverse tunnel" primitive) rather than building two bespoke, direction-specific solutions. Recommend cross-referencing/consolidating design work between the two.
Candidate approaches (need a design spike, not assumed as decided):
--add-host=host.docker.internal:host-gateway at container-create time (Linux; Docker Desktop macOS/Windows already auto-injects this), then have deacon write/inject a small in-container proxy (e.g. socat TCP-LISTEN:<local_port>,fork,reuseaddr TCP:host.docker.internal:<host_cdp_port>) so the agent can still dial 127.0.0.1:<PORT> as the request expects. Podman's rootless netns gateway needs separate handling.
Extend relay.rs for container-initiated dial-out, mirroring the existing daemon/registry/marker lifecycle but reversing the dial direction — reuses proven daemon plumbing (daemonize(), SIGTERM-on-teardown, user_data_folder-scoped marker files) at the cost of nontrivial changes to relay.rs's current host→container assumption.
2. MCP config injection — likely out of deacon's scope, should be a Feature instead
Writing ~/.config/Claude/mcp.json with a chrome-devtools block hardcodes assumptions about one specific AI tool's config format into deacon core. Grep confirms zero existing references to claude, Claude, mcp.json, or MCP anywhere in the codebase — deacon has no precedent of special-casing any single IDE/agent/tool (consistent with the Consumer-Only Scope constitution: deacon implements the containers.dev spec surface, not app-specific tooling).
The sanctioned extension point for "install/configure app X inside the container" already exists: the Feature mechanism (crates/core/src/features.rs — FeatureMetadata, InstallationPlan, FeatureMerger; invoked via install_features_for_compose in up/compose.rs:748 and apply_features_and_lockfile in build/mod.rs:1610). Writing an MCP config for chrome-devtools-mcp is exactly the shape of thing a Feature's install.sh should do — a workspace opts into a chrome-devtools-mcp Feature in devcontainer.json, and that Feature's install script writes whatever config file the agent it targets expects.
What deacon should own, if anything, is exposing the forwarded host port/address to the container in a discoverable way (e.g. via remoteEnv, following the existing containerEnv/remoteEnv PATH-append precedent in container_env_probe.rs:593-648) — e.g. a DEACON_HOST_CDP_PORT or similar env var — so a Feature or postCreateCommand can consume it without deacon needing to know anything about MCP, Claude, or any other specific agent.
Security — this needs prominent, explicit treatment
The Chrome DevTools Protocol has no built-in authentication. Anything with network access to a CDP endpoint can execute arbitrary JavaScript in any open tab, read cookies/session storage for any site the user is logged into, and navigate to file:// URLs to read local files — this is why Chrome's own docs and multiple public security advisories warn against binding --remote-debugging-port beyond loopback. Making that endpoint reachable from inside a devcontainer (which may run code from an untrusted repo, a compromised Feature, or a prompt-injected agent) is a materially larger exposure than the existing port-forward or clipboard-bridge proposals, both of which are loopback-only and narrowly scoped.
This should follow the same trust-boundary precedent already established for --auto-forward's browser auto-open (SECURITY.md:104-131): forwarded-port binds are loopback-only, and the machine owner controls the sensitive knob (DEACON_BROWSER env var / settings.json, never workspace-sourced) even though the workspace can request the behavior (onAutoForward). Applied here:
The flag enabling this bridge must be explicit, opt-in, and off by default — never auto-enabled by a workspace's devcontainer.json.
Loud documentation (in --help and SECURITY.md) of exactly what the flag exposes: any process in the container gets full remote-control of the host browser session.
No silent default port (the request suggests 9222/4222 ambiguously) — require the user to specify which host CDP port to bridge, and fail loudly if unreachable rather than silently no-oping (Principle IV).
Consider whether deacon should refuse to proceed unless the host Chrome was launched with --remote-debugging-address=127.0.0.1 (never 0.0.0.0) and, ideally, --remote-allow-origins scoped — though deacon doesn't control how the user launches Chrome, so this may only be enforceable via a warning, not a hard check.
Open questions
Is deacon launching/managing the host Chrome process in scope at all, or does the user launch it themselves (google-chrome --remote-debugging-port=9222) and deacon only bridges to an already-running instance? (Recommend: deacon does not manage host GUI apps — bridge-only.)
Confirm the actual Claude Code MCP config path/schema before any Feature is written — ~/.config/Claude/mcp.json in the original request has not been verified against current Claude Code conventions and may be stale/incorrect.
Phase 1 — Reachability only: opt-in flag on deacon up/exec (e.g. --host-cdp-port <PORT>), loopback-only bridge, port/address exposed to the container via remoteEnv, no MCP/Claude-specific code in deacon core. Security documentation in SECURITY.md from day one.
Phase 2 (separate effort, likely a community Feature, not deacon core): a chrome-devtools-mcp Dev Container Feature that consumes the exposed env var and writes the appropriate MCP config for whichever agent's format is targeted.
Acceptance criteria (Phase 1 only — deacon core)
Bridge is fully opt-in; deacon up/exec with no flag is byte-identical to today's behavior.
Host-side bind (if any new listener is introduced) is loopback-only, matching the port_forward daemon's existing posture.
The forwarded port/host address is discoverable inside the container via an env var (name TBD), with no deacon-side knowledge of MCP, Claude, or chrome-devtools-mcp baked in.
SECURITY.md documents the exposure explicitly before merge, not as a follow-up.
down/container teardown leaves no orphaned host-side process or listener, matching the port_forward daemon's cleanup guarantee.
cargo clippy --all-targets --all-features -- -D warnings and cargo fmt --all -- --check clean; new integration tests correctly grouped in .config/nextest.toml.
Problem
An agent (e.g. Claude Code) running inside a
deacon-managed devcontainer cannot drive a Chrome instance running on the developer's host desktop — there's no network path from the container to a host process, and no standard way to tell an in-container MCP client where to find it. This blocks browser-automation workflows (visual debugging, screenshot loops, page-navigation QA) that assume the agent can reachchrome-devtools-mcppointed at the host browser's--remote-debugging-port.Note on the original ask: the request specifies
deacon shell --browser. That subcommand doesn't exist —crates/deacon/src/cli.rs:245'sCommandsenum lists onlyUp, Build, Exec, ReadConfiguration, Config, Templates, Upgrade, SetUp, RunUserCommands, Down, Doctor, Outdated, ForwardDaemon. This issue reframes the flag as living ondeacon up(and/ordeacon exec), consistent with the real subcommand surface.Two problems bundled together — recommend splitting scope
The request bundles (1) a network-reachability problem and (2) an app-specific config-injection problem. These have very different risk profiles and should likely be split:
1. Container→host reachability (new plumbing, needs its own design)
This is the reverse direction of everything
deaconcurrently does for port handling. The existing015-auto-forward-portsdaemon (crates/core/src/port_forward/{daemon.rs,relay.rs,registry.rs}) solves host→container reachability: the host binds a loopback listener and, per connection, runsdocker exec -i <id> nc/socat ...to dial into the container's netns (relay.rs:1-6,56, the relay program dials{dial_host}:{container_port}from inside the container, driven by a host-side listener). There is currently zero code path for the opposite direction — confirmed via grep: no hits forhost.docker.internal,host-gateway,add-host, orextra_hosts/ExtraHostsanywhere indocker.rs,compose.rs,container.rs, orup/*. Making a container process reach a host port is genuinely new work, not an extension of the existing daemon's direction.This same container→host reachability gap is also the crux of a related open issue (host clipboard bridge, #281) — worth designing once and sharing the underlying mechanism (e.g. a generic "reverse tunnel" primitive) rather than building two bespoke, direction-specific solutions. Recommend cross-referencing/consolidating design work between the two.
Candidate approaches (need a design spike, not assumed as decided):
--add-host=host.docker.internal:host-gatewayat container-create time (Linux; Docker Desktop macOS/Windows already auto-injects this), then have deacon write/inject a small in-container proxy (e.g.socat TCP-LISTEN:<local_port>,fork,reuseaddr TCP:host.docker.internal:<host_cdp_port>) so the agent can still dial127.0.0.1:<PORT>as the request expects. Podman's rootless netns gateway needs separate handling.relay.rsfor container-initiated dial-out, mirroring the existing daemon/registry/marker lifecycle but reversing the dial direction — reuses proven daemon plumbing (daemonize(), SIGTERM-on-teardown,user_data_folder-scoped marker files) at the cost of nontrivial changes torelay.rs's current host→container assumption.2. MCP config injection — likely out of deacon's scope, should be a Feature instead
Writing
~/.config/Claude/mcp.jsonwith achrome-devtoolsblock hardcodes assumptions about one specific AI tool's config format into deacon core. Grep confirms zero existing references toclaude,Claude,mcp.json, orMCPanywhere in the codebase — deacon has no precedent of special-casing any single IDE/agent/tool (consistent with the Consumer-Only Scope constitution: deacon implements the containers.dev spec surface, not app-specific tooling).The sanctioned extension point for "install/configure app X inside the container" already exists: the Feature mechanism (
crates/core/src/features.rs—FeatureMetadata,InstallationPlan,FeatureMerger; invoked viainstall_features_for_composeinup/compose.rs:748andapply_features_and_lockfileinbuild/mod.rs:1610). Writing an MCP config forchrome-devtools-mcpis exactly the shape of thing a Feature'sinstall.shshould do — a workspace opts into achrome-devtools-mcpFeature indevcontainer.json, and that Feature's install script writes whatever config file the agent it targets expects.What deacon should own, if anything, is exposing the forwarded host port/address to the container in a discoverable way (e.g. via
remoteEnv, following the existingcontainerEnv/remoteEnvPATH-append precedent incontainer_env_probe.rs:593-648) — e.g. aDEACON_HOST_CDP_PORTor similar env var — so a Feature orpostCreateCommandcan consume it without deacon needing to know anything about MCP, Claude, or any other specific agent.Security — this needs prominent, explicit treatment
The Chrome DevTools Protocol has no built-in authentication. Anything with network access to a CDP endpoint can execute arbitrary JavaScript in any open tab, read cookies/session storage for any site the user is logged into, and navigate to
file://URLs to read local files — this is why Chrome's own docs and multiple public security advisories warn against binding--remote-debugging-portbeyond loopback. Making that endpoint reachable from inside a devcontainer (which may run code from an untrusted repo, a compromised Feature, or a prompt-injected agent) is a materially larger exposure than the existing port-forward or clipboard-bridge proposals, both of which are loopback-only and narrowly scoped.This should follow the same trust-boundary precedent already established for
--auto-forward's browser auto-open (SECURITY.md:104-131): forwarded-port binds are loopback-only, and the machine owner controls the sensitive knob (DEACON_BROWSERenv var /settings.json, never workspace-sourced) even though the workspace can request the behavior (onAutoForward). Applied here:devcontainer.json.--helpandSECURITY.md) of exactly what the flag exposes: any process in the container gets full remote-control of the host browser session.9222/4222ambiguously) — require the user to specify which host CDP port to bridge, and fail loudly if unreachable rather than silently no-oping (Principle IV).--remote-debugging-address=127.0.0.1(never0.0.0.0) and, ideally,--remote-allow-originsscoped — though deacon doesn't control how the user launches Chrome, so this may only be enforceable via a warning, not a hard check.Open questions
google-chrome --remote-debugging-port=9222) and deacon only bridges to an already-running instance? (Recommend: deacon does not manage host GUI apps — bridge-only.)remoteEnv, a generated file under.devcontainer/, or both?~/.config/Claude/mcp.jsonin the original request has not been verified against current Claude Code conventions and may be stale/incorrect.Suggested implementation phases
deacon up/exec(e.g.--host-cdp-port <PORT>), loopback-only bridge, port/address exposed to the container viaremoteEnv, no MCP/Claude-specific code in deacon core. Security documentation inSECURITY.mdfrom day one.chrome-devtools-mcpDev Container Feature that consumes the exposed env var and writes the appropriate MCP config for whichever agent's format is targeted.Acceptance criteria (Phase 1 only — deacon core)
deacon up/execwith no flag is byte-identical to today's behavior.port_forwarddaemon's existing posture.chrome-devtools-mcpbaked in.SECURITY.mddocuments the exposure explicitly before merge, not as a follow-up.down/container teardown leaves no orphaned host-side process or listener, matching theport_forwarddaemon's cleanup guarantee.cargo clippy --all-targets --all-features -- -D warningsandcargo fmt --all -- --checkclean; new integration tests correctly grouped in.config/nextest.toml.