Releases: pacepace/aidc
Release list
v1.8.0
Reaching a database on another network
A proxied session can now reach a TCP service the host can reach — a database over ZeroTier, a VPN, the LAN — and nothing else. Name it in your own ~/.config/aidc/config.yaml (egress_tcp:), at create (--egress-tcp host:port), or on a running session (aidc egress <session> add host:port). Inside the session you connect exactly as you would outside; TLS stays end to end, and every connection is logged to the session's audit dir. The session still has no route out.
Security
- A repo's own
.aidc/config.yamlcan no longer widen the sandbox. From a workspace or repo config only safe or tightening settings apply; anything that would open a path out is listed byaidc createand ignored unless you pass--trust-repo-config. - A session can no longer plant a hook that runs on your machine.
~/.claude/settings.jsonwas bind-mounted read-write into every session; it is now copied in when the session is created. Existing sessions keep the old mount throughaidc upgrade;aidc kill+aidc createcloses it. - New package versions wait 14 days. The MCP server's dependencies are upgraded past ten advisories, and aidc never adopts a version published in the last two weeks — in its own builds and, as a system default for uv, pip and npm, inside sessions. Claude Code is exempt and stays current. One flag overrides per command; see the README.
- Subdomains of known-malware domains are blocked too. Squid matched listed domains exactly, so
www.<listed>went through. - A
falsesetting inconfig.yaml(tld_taints,claude_resume,share_*) was silently ignored on machines withyqinstalled. Fixed.
Fixed
- The proxy no longer drops every connection for ~20 seconds when the malware list refreshes (#34). Squid asks a small lookup helper that follows the list on disk, so it never reloads.
aidc create --portworks on a machine that never ranaidc proxy.- Your Claude Code status line shows inside sessions.
Upgrading
aidc update, then aidc rebuild, restart aidc mcp, and recreate sessions with aidc kill + aidc create — aidc upgrade keeps a session's proxy and its old settings mount, so it does not pick up the proxy or settings changes. Sessions reaching a database need the new proxy stack.
Full details: CHANGELOG.md [1.8.0].
v1.7.0
Replies from a shared session used to arrive late, twice, or not at all. This release
decides what a session is doing from Claude's transcript instead of watching its screen,
makes the send queue never drop a prompt, and fixes every delivery path that lost a reply.
It was tested end to end against a real orchestrator, with two agents driving both sides
through twelve scenarios. That test found five bugs; all of them are fixed here.
Replies arrive, once.
- Webhooks survive an
aidc-mcprestart and pick up where they left off, so a reply
written while the MCP was down is delivered rather than skipped. - The first reply after Claude restarts inside a session is no longer lost.
- A reply left in a damaged transcript (a torn write, a compaction) is delivered before
the watcher follows Claude onto a new file. session_resendcan reach a session's earlier transcripts.
Interrupts reach the orchestrator. Press Esc after Claude has started writing and the
partial answer is delivered, marked interrupted. Press Esc before it writes anything and
that is recognised too, from the prompt Claude puts back in the input box, within seconds.
A queued prompt is never lost. No deadline; held while Claude restarts; failed pastes
retried; saved across an MCP restart. If its session is removed, the conversation that
sent it is told. If it has waited ten minutes, it is told that too. aidc upgrade keeps a
session's queue instead of looking like a kill.
What you type in a session is visible to the orchestrator, and with
metallm.send_speaker it is recorded as yours instead of answered. Off by default.
Failures say what kind they are. Every failed tool result carries an error_code, so
a caller can tell "retry" from "tell the person" without reading the sentence.
Dev containers cannot reach the MCP control plane through their proxy, for sessions
whose traffic goes through it.
session_create over MCP works, and only when you allow it. It never worked in a
released image. It is now offered only with mcp.session_create: true, which mounts your
home into the MCP container, and it refuses a repo or workspace outside that directory.
aidc kill removes a session's networks even when a port forward is attached, instead
of reporting success and leaving one behind.
Upgrading: run aidc rebuild and aidc mcp restart. The container-side fixes do not
take effect until that image is replaced. Existing sessions keep working; they pick up the
new dev image on aidc upgrade <session>.
v1.6.0
Your scratchpad now follows a session across the container boundary
share_memory has always carried a session's conversation between your host and the dev container. Its working files never followed.
Claude Code keeps them in /tmp/claude-<uid>/<encoded-repo>/<session-id>/ — scratchpad/ and tasks/. Inside a session that path lived on the container's writable layer, so aidc upgrade and aidc kill destroyed it. Pop a session out to your host and resume it and you kept the transcript but lost everything the session had been building.
aidc create now bind-mounts your per-repo scratchpad at the identical path inside the container. Pop a session out, resume it on the host, and it finds its own files; pop it back in and they are still there.
Nothing to configure — it is on by default, like share_memory. Turn it off with share_scratchpad: false.
Bounded on purpose
Only the current repo's subdirectory is bridged, never the whole /tmp/claude-<uid> root, which holds every other project's scratchpad. That is the same line aidc already draws in refusing to share all of ~/.claude with the sandbox.
It is a live read-write host path, chosen over a one-way copy because a copy cannot carry work back in — which is half the point. The exposure is one directory, and it is the same trade the repo mount already makes.
It cannot cost you a session
Both directory levels sit under /tmp, which is world-writable. A path already held by another user, or a planted symlink, is refused rather than followed, and every failure costs only the bridge — never the session you were trying to create.
Known limits
- macOS: the bridge is currently skipped. It requires your host uid to match the container's
vscode(1000); on Docker Desktop, which remaps bind-mount ownership, that check is more conservative than it needs to be. Recorded as a known gap. - Existing sessions do not gain the mount.
aidc upgradereuses the compose file rendered when the session was created. Runaidc kill+aidc createto pick it up. - The bridged directories live on your host and are not removed by
aidc kill— that is what makes popping back in work. Nothing sweeps them, so they accumulate until a reboot clears/tmp.
Full detail in the changelog.
v1.5.1
Changed
The docs and the MCP tool text describe "the orchestrator", not a specific product. The README sections on callback delivery now document the contract itself for any MCP-capable orchestrator: the endpoint shape, the bearer, and both payload shapes (session_invoke_async's and the session watcher's, including prompt_origin). Examples use neutral names, and the tool descriptions and error messages an orchestrating model reads say "the orchestrator".
The metallm.callback_url config key is unchanged and documented as keeping its historical name, so no configuration needs to change.
Upgrading
aidc update && aidc rebuild
aidc mcp stop && aidc mcp start # the reworded tool text lives in the MCP imageRunning sessions need nothing; aidc upgrade <name> when convenient.
Full changelog: v1.5.0...v1.5.1
v1.5.0
Changed
The dev container owns its Claude login. Each session keeps Claude's config directory on its own volume (CLAUDE_CONFIG_DIR=/home/vscode/.claude, the layout Anthropic's reference devcontainer uses) and you /login inside it once:
aidc attach my-feature
# window 0: /login → open the URL, paste the code backThat login refreshes itself, survives aidc restart and aidc upgrade, works with Remote Control, and can be a different account from the host's. Run /login again inside the session to switch accounts, for example when one hits its session limit.
Why: the host's .credentials.json and ~/.claude.json used to be bind-mounted into the container. Claude Code replaces both files by rename, so the mounted credentials went stale on the host's first refresh (anthropics/claude-code#18443) and could not be written from inside, and the read-only ~/.claude.json could never record a new account. That is why in-container logins expired after a few hours and /login with another account never took.
Onboarding state (theme, output style, this project's trust decision) is seeded once from the host's ~/.claude.json, without the account, API keys, MCP server definitions or prompt history, so the first launch goes straight to the login prompt. Per-project memory and conversation history, settings.json and plugins are bridged exactly as before. The long-lived token path (aidc claude-token) is unchanged and still skips the login, at the cost of Remote Control.
Fixed
aidc upgradenow actually moves a session onto the new version's image. It recreated the dev container from the create-time compose file, which pins the image at the tag current back then, so across a version bump the container came back on the old image while the command reported the new one.
Removed
aidc auth-bridge,aidc reauth, the macOS Keychain extraction, and theshare_authconfig key. With nothing bridged there is nothing to keep fresh. A watcher left running by an earlier aidc on macOS is retired by the nextaidc createoraidc kill.
Upgrading
aidc update && aidc rebuild
aidc mcp stop && aidc mcp startThen, per running session, either aidc upgrade <name> (its old host login mounts are removed; the next launch runs Claude's onboarding and asks for /login) or aidc kill <name> + aidc create <name> ... for the seeded start.
Full changelog: v1.4.1...v1.5.0
v1.4.1
Fixed
A prompt typed straight into a watched session no longer reaches the orchestrator as an unexplained reply.
If you aidc attach into a session that MetaLLM is driving over session_send and type into the Claude window yourself, the reply used to arrive at the orchestrator with no trace of your prompt. It read the reply as an answer to whatever it had last sent and could not tell where the new instructions came from.
In the JSONL transcript a pasted prompt and a typed one are identical, so the MCP now keeps a record of every prompt it injects itself (<session>.sent-prompts.json beside the watermarks, bounded to 200 entries / 24 h). At delivery, any prompt on the turn that is not in that record is prepended to the content under a note saying the user typed it at the terminal, so the reply reads in context.
- Slash commands render as
/name args. - Hook feedback, task notifications, auto-continues, interrupt markers, and any wrapped line (local-command output,
!bash-mode input and output) are never attributed to the person. - The callback payload gains
prompt_origin(terminal/orchestrator/""). It is additive; an existing MetaLLM keeps working without changes. - Exactly-once delivery is unchanged: the ledger still keys on the bare reply.
Upgrading
aidc update && aidc rebuild
aidc mcp stop && aidc mcp start # the watcher fix lives in the MCP imageThen aidc upgrade <session> per running session, or aidc kill + aidc create.
Full changelog: v1.4.0...v1.4.1
v1.4.0
Added
aidc update updates the CLI itself, the way brew upgrade or apt upgrade would. It detects how this copy was installed and runs the matching step:
- release installer under
~/.local/share/aidc/: re-runs the latest release'sinstall.sh, which installs alongside the old version and flips the symlink - Homebrew:
brew upgradeon the installed formula (aidcoraidc@X.Y) - git checkout:
git pull --ff-only
aidc update --check reports the installed and latest versions and changes nothing. The CLI and the images are separate, so follow an update with aidc rebuild, then aidc upgrade <session> per running session.
This exists because the failure is quiet: a stale installed copy rebuilds images from its own old tree, and nothing tells you the checkout you just pulled is not the aidc on your PATH.
The Homebrew tap updates itself on every release, with no token to rotate. The workflow now pushes the tap over SSH with a write deploy key that never expires and can reach nothing else. release/tap-deploy-key.sh creates or rotates it and prints only the fingerprint. Dry-run dispatches now prove push authorization, not just clone access.
Upgrading
Once on 1.4.0, future upgrades are one command:
aidc update && aidc rebuildTo get onto 1.4.0 from an earlier release, run the installer one-liner from the README (or brew upgrade aidc, or git pull), then aidc rebuild. Then aidc upgrade <session> per session, or aidc kill + aidc create to start from the new bake.
Full changelog: v1.3.1...v1.4.0
v1.3.1
Fixed
aidc rebuild now actually refreshes Claude Code. The dev-base image installs Claude Code with Anthropic's native installer, but that step was an ordinary cached Docker layer and nothing above it changes between releases. Every rebuild silently reused the version fetched the first time the layer was built, so an image rebuilt today could still carry a release from a month ago. The install step now takes a CLAUDE_CODE_REFRESH build arg used purely to invalidate that layer, and aidc rebuild passes a fresh value each run. Everything above stays cached; only the installer re-runs.
Note that a session's home directory is a named volume seeded from the image only on first create, so aidc upgrade keeps the session's existing Claude Code. Existing sessions pick up new releases through Claude Code's in-session auto-update; a fresh aidc create starts from the newly baked version.
Upgrading
git pull && aidc rebuild # or: brew upgrade aidcThen aidc upgrade <session> per session, or aidc kill + aidc create to start from the new bake.
Full changelog: v1.3.0...v1.3.1
v1.3.0
A security release. Upgrade if you rely on aidc's egress filtering — until now
it could be bypassed trivially, and silently.
The proxy was advisory
aidc routed the dev container's HTTP through Squid by setting HTTP_PROXY. That
is a request, not a constraint. From a real session:
$ env -u HTTP_PROXY -u HTTPS_PROXY -u http_proxy -u https_proxy \
curl -s -o /dev/null -w '%{http_code}' https://example.com/
200
$ grep -c example.com /var/log/squid/access.log
0
Four env -u flags and the sandbox was open. Worse than the bypass itself is the
second line: zero entries in the access log. The blocklist never applied, and
because the policy sidecar tails that log, taint detection was blind to it. Squid
only ever saw the traffic that asked to be seen.
Anything ignoring those variables had the same free pass — a library that doesn't
read them, a raw socket, a statically linked binary. No agent malice required.
NET-10 claimed processes inside the dev container "cannot reconfigure or bypass"
the proxy. That was not true, and had not been true in any released version.
Egress is now enforced by topology
The session bridge is a Docker internal network. Docker installs no
masquerade rule for it, so there is no route off that bridge at all. Squid is
dual-homed onto a separate egress network and is the only way out.
This is deliberately not a firewall rule. The dev container runs --privileged
(it needs it for DinD), so anything inside it can flush any rule it can see —
enforcement has to live where the agent cannot reach. Attacked as root, with the
proxy variables stripped, an explicit default route added via Squid, ip_forward
enabled, and its own MASQUERADE installed:
| attempt | result |
|---|---|
| direct HTTPS, proxy env stripped | blocked |
raw TCP to 1.1.1.1:80 |
Network is unreachable |
root + own default route + MASQUERADE |
still blocked |
| proxied HTTP | 200 |
blocklisted .cn via Squid |
403 |
Only squid, refresher (fetches threat feeds) and policy (POSTs the taint
webhook) join the egress network. dev and audit never do.
The test that should have caught it
make smoke had a step called "proxy enforcement" that passed on every release.
It only ever tested the proxied path — curl -x http://aidc-proxy:3128 — which
succeeds just as happily on a sandbox enforcing nothing, because Squid answers
when you ask it to. Nothing checked that going around Squid failed.
The suite now asserts that stripping the proxy variables fails, that raw TCP to a
public IP fails, and that Squid is still reachable, so it can no longer go green
on this class of bug.
Attaching networks still works
aidc create --network and aidc network <session> add are unchanged. A session
still reaches an attached project's postgres, NATS or redis by container name, on
any port.
Because most compose bridges are NATed — and an internal bridge has no default
route of its own — attaching one does restore general internet egress as a side
effect. aidc cannot prevent that; it does not own that network. Detaching closes
it again, and the smoke suite now asserts exactly that scoping. Attach the
narrowest network that does the job.
Escape hatch
aidc create myproj --egress direct # or `egress: direct` in .aidc/config.yamlRestores the pre-1.3.0 NATed bridge for sessions needing reachability an attached
network cannot provide — ZeroTier/Tailscale hosts, direct DNS. aidc create states
plainly that enforcement is off for that session.
Compatibility
Declared --port forwards and aidc proxy are unchanged from your side, but are
now dual-homed aidc/forwarder sidecars — an internal network cannot publish
ports, so ports: on the dev service would have silently done nothing. DinD image
pulls route through Squid (the inner daemon already inherits the proxy env).
Existing sessions keep their current behaviour until recreated. aidc kill +
aidc create picks up enforcement; aidc upgrade does not, since it reuses the
compose file rendered at create time.
Existing sessions are not converted
aidc upgrade reuses the compose file rendered at create time, so upgrading a
session created before 1.3.0 leaves it on its old NATed bridge — upgraded, but
still bypassable. aidc status <name> now reports which posture a session is in,
and aidc upgrade warns before preserving an unenforced one.
To convert: aidc kill <name> && aidc create <name> ...
v1.2.0
A feature release: an aidc session can now reach a stack that is already running
elsewhere on the host.
Sessions can reach another stack's services
Until now a session's dev container sat alone on aidc-<session>-net, and
aidc proxy only went host → container. There was no way for a session to talk
to a database, queue, or cache belonging to another compose project — which is
exactly what you need when the thing you are debugging is that stack.
A session's dev container can now attach to named Docker bridge networks and
resolve that project's containers by name.
Declared — survives restart, upgrade, and recreate:
aidc create metallm --network metallm_default# or in .aidc/config.yaml
networks:
- metallm_defaultRepeatable, and the CLI merges with config rather than overriding it (unlike
--dns) — every named network is something the session needs, so more sources
means more networks. Networks are validated immediately after config load,
before any image build, so a typo costs you a message rather than a full
dev-base rebuild.
Adhoc — attaches a session that is already running, no recreate:
aidc network metallm add metallm_default
aidc network metallm ls # marks each attachment declared or adhoc
aidc network metallm rm metallm_defaultThen, from inside the session: psql -h metallm-postgres-1 -U metallm.
aidc restart |
aidc upgrade |
aidc kill + create |
|
|---|---|---|---|
| declared | survives | survives | survives |
| adhoc | survives | lost | lost |
Adhoc attachments survive a restart, unlike adhoc port forwards which
aidc restart sweeps. The difference is real rather than an inconsistency: a
port forward is a separate sidecar container pointed at a container that is
going away, while a network endpoint is part of the dev container's own config
and comes back with it. aidc upgrade now names the adhoc attachments it is
about to drop, with the commands to re-add them.
aidc status <name> gained an attached networks section, listed even when
empty.
The gateway pin
Every attachment pins gw_priority so aidc-<session>-net keeps the default
route. This is the load-bearing part of the change, and it is not what the
obvious implementation would have done. Measured on Docker 29.1.3:
| default route ends up on | |
|---|---|
docker network connect ext dev |
ext — steals it immediately |
Compose priority: 100 on the aidc net |
ext — steals it anyway |
--gw-priority / Compose gw_priority |
the aidc net — correct |
Compose's priority only orders the connect sequence; it does not choose the
gateway. Without the pin, attaching a network silently reroutes all of the
session's egress — squid-proxied traffic included — out through someone else's
bridge, with nothing in the audit trail to show for it. A unit test and a smoke
assertion both pin this, the latter by reading the container's own
/proc/net/route.
gw_priority needs Docker Compose 2.34+, so aidc create probes for it and
fails with that reason rather than rendering a file whose pin would be silently
dropped. Sessions with no attachment render a byte-identical compose file to
before, so they neither change behaviour nor acquire a new Compose version
floor.
This widens the sandbox, on purpose
Everything on an attached network is reachable from the session on every
port. That traffic does not pass through squid, so the blocklist does not
apply and taint detection cannot see it. The attachment is bidirectional.
Only the dev service is ever attached — squid, refresher, policy, and audit
stay isolated on the session's own network. host, none, and Docker's default
bridge are refused outright. The CLI help, the config template, aidc create's
output, and aidc status all say this at the point of use, and
docs/done/design-07-safety-model.md now sizes the residual risk honestly
instead of implying the proxy covers everything.
Attach the narrowest network that does the job, and treat anything on it as
being inside the blast radius.