All notable changes to Construct CLI will be documented in this file.
- microVM: construct now matches msb 0.6.15 (schema, refs, workdir): construct embeds the microsandbox engine through the Go SDK, pinned at v0.6.10, while the host
msbCLI could be newer. A newer CLI migrates~/.microsandbox/dbforward and the embedded engine then fails every daemon create withdatabase schema is newer than this msb binary(hit live on a Linux host running msb 0.6.15). The SDK pin is now v0.6.15; when upgrading the host msb, upgrade construct in the same window. - No more phantom 3.5GB image transitions:
msb image tagdoes not exist in msb 0.6.15 (and the pull+tag flow was dead code), so a successful GHCR pull always "failed" its tag step and construct fell through to the docker save + load transition.msb load -ialso imports archives under thelocalhost/prefix, which the bare-ref probe never matched, so every run re-transitioned the archive. Image resolution now probes candidates in order (bare,ghcr.io/estebanforge/construct-box:latest,localhost/construct-box:latest) for EnsureImage, the daemon run spec, the prepull, andct sys doctor; the pull path verifies the registry ref and the load path verifies the localhost ref, with no tag step at all. Known follow-up: the docker-archive transition still produces an incomplete rootfs under 0.6.15 (guest has no/bin/sh); the registry pull is the reliable path and is preferred. - Daemon create no longer dies on image WORKDIR validation: msb 0.6.15 validates the image working directory at create time and construct-box declares
WORKDIR /projects, which the transitioned archive fails. Sandbox create now sets an explicit workdir (/home/construct, present in every construct-box image); exec paths set their own cwd, so this is only the default. - Boot telemetry is now collectable:
msb-boot:lines printed to stderr only, so the dogfood P0 greps overlogs/*.logmatched nothing even on a healthy install.msbLogBootnow also appends an RFC3339-stamped line to~/.config/construct-cli/logs/msb-boot.log; the format test isolates HOME so fixture lines cannot pollute the real log.
docs/VMsv2.mdsection 10: preliminary single-run observations recorded (macOS recreate 490s with the skills-drift recreate firing correctly, reconnect 0s on both hosts) plus the collection caveat that the log-file telemetry ships with this release.
- Host skills mount: bind the host's skills library into every supported agent's skills dir:
~/Dev/EstebanForge/AGENTS/skills(or any other configured source) is now bind-mounted at create time into/home/construct/<agent>/skillsfor ten supported agents (agy, claude, amp, qwen, copilot, crush, droid, goose, kilocode, cline). Source resolution precedence:$CONSTRUCT_SKILLS_SOURCEenv var, then[sandbox] skills_source, then auto-detect (~/Dev/EstebanForge/AGENTS/skills,~/AGENTS/skills,~/.config/construct-cli/skills,$XDG_DATA_HOME/construct/skills). Mount is read-only by default; opt into read-write via[sandbox] skills_read_only = falsewhen an agent must author skills. Docker compose uses:ro(or:ro,zon Linux SELinux); microVMmsb.Mount.Bindhonors the same flag. Fails closed (no mount, no error) when the source does not resolve. Managed entirely by construct-cli;manage.shno longer needs to copy skills into the sandbox home. - Daemon recreate parity for skills mounts: a new label
construct.daemon.skills_hash(hash of source + read-only flag + target count) is stamped byBuildMsbRunSpecwhenever skills are enabled, andmsbDaemonNeedsRecreatechecks it FIRST in both multi-path and single-path modes. Toggling skills, flipping RO/RW, source appearance, or supported-agent-list growth now recreate the running daemon so the new mounts take effect (previously the change was invisible until manual recreate). P2.2 wire-in (combined mount-set resolution inmsbSandboxMounts+MsbPathMaps) is still pending; the hash surface is ready for it. - Boot telemetry (
msb-boot:log line):EnsureMsbDaemonnow emits a stablemsb-boot:line at every return path with the outcome (cold | recreate | warm | reconnect), elapsed seconds, mount count, and (forrecreate) the reason.msbBootClockis injectable so tests run deterministic without sleeping. Output goes to stderr (run-path rule respected). Numbers land indocs/VMsv2.mdsection 10 once the dogfood week collects medians; P6 (snapshot fork) is gated on those numbers. - Widened daemon flock (phase 1): a blocking
syscall.Flockon~/.config/construct-cli/daemon.lock(mode 0600) is acquired at the top ofEnsureMsbDaemonviadefer releaseLock(). The critical section now wraps read state, decide, write state, and the recreate/boot itself so concurrentctinvocations learning different roots cannot produce last-write-wins root loss or a double recreate. A 250ms "waiting for another construct invocation" notice fires on slow acquires.ct sys doctorsurfacesdaemon lock: free/held(per-request probe, intentionally not transactional with the actual lock). - Learned roots store (phase 2 data layer):
roots.jsonis a versioned, atomic-write JSON store at~/.config/construct-cli/roots.jsonwith LRU eviction ([daemon] max_learned_roots, default 8).requestLearnRootenforces the workspace guard, prompts via gum when interactive, denies with an actionable message when non-interactive. The fullEnsureMsbDaemonwire-in (P2.2) is pending; the helper is shipped withnolint:unusedand unit tests covering the no-op paths. - CLI surface for learned roots:
construct sys daemon roots list(table of root, source, last used, mount dest) andconstruct sys daemon roots forget <path>(refuses configureddaemon.mount_pathsentries). Pinned paths and forgotten paths are shown side by side; themanage.shsymlink/copy flow for skills stays in place as a fallback for non-construct run paths. - Idle stop (phase 3): nothing runs while the user is away. A session registry at
~/.config/construct-cli/sessions/<pid>.json(one file per livect) is read+written only inside the daemon flock critical section.execViaMsbDaemonregisters the current PID;Teardownunregisters and, on the LAST unregister, spawns a detachedconstruct sys daemon idle-watchprocess (true daemon viaSetsid, stdin/stdout/stderr closed,Process.Release). The watcher sleeps[daemon] idle_stop_minutes(default 45; 0 disables), rechecks the registry every 30s, and stops the daemon only if the count is still zero at the deadline. A newctinvocation that registers a session during the sleep stands the watcher down. The watcher acquires the flock before stopping (round 8 fix) so a freshEnsureMsbDaemoncannot be torn down mid-flight, and re-checks the live count under the lock. Bridges (SSH proxy, clipboard, host exec, herdr) die with the daemon, so idle = smaller host attack surface. - Background image prepull (phase 4): after a successful
construct update, a detachedmsb pull+msb image tagofconstruct-box:latestis spawned so the nextctfinds the image already staged. Opt-out via[runtime] prepull_image = false. Output goes to~/.config/construct-cli/logs/prepull.log. The detached child usesos.Executable()to re-exec the freshly installed binary (verified:installBinaryWithBackupfinishes its atomic rename before the prepull spawn at line 385, so the ordering invariant is correct). The prepull logs a warning when[runtime] prepull_image = truebut[runtime] backendis not"microvm"(the default"auto"falls through to OCI runtime and never resolves to microvm, so the silent skip is now visible). - Credential proxy design (phase 5, design only):
docs/CREDS-PROXY.mdcovers the full design: threat model, component diagram, CA lifecycle (host-generated, 1y validity, embedded in image trust store), per-provider rule format withstrip_request_authflag, keychain-backed token store (macOS Security / Linux Secret Service / permissions-protected file fallback), four-phase rollout plan withruntime.credential_proxyflag, network-mode interaction. Implementation is deferred to a follow-up; P5.2 (peer review) is the next step.
- Daemon flock: no more false "Waiting for another construct invocation" notice on every long boot: the 250ms notice goroutine was disarmed only at
release()time instead of at acquire time, so ANY lock hold longer than 250ms — which is every real daemon boot — armed the timer for the full duration and printed the waiting notice 250ms in, uncontended or not. The notice now measures the ACQUISITION wait only (disarm via a dedicatedsync.Oncefired on acquire success or error), and the goroutine captures the stderr writer before spawn instead of reading theos.Stderrglobal late, which also fixes thego test -racedata race against tests that swapos.Stderr. - Doctor: no more duplicate "Container Runtime" line and wrong "Daemon Mode" wording under microvm: the runtime check was appended twice when
backend = "microvm"(once inside the msb branch, once at the end of the shared chain), soct sys doctorprinted "Not applicable (runtime backend = microvm)" on two consecutive lines. The Daemon Mode check then showed "Unavailable (config/runtime missing)" — misleading on microvm, whereruntimeNameis deliberately blanked: the check now reports "Not applicable (runtime backend = microvm)" and points atconstruct sys daemon statusfor the real microVM daemon state. - Doctor: stale SSH agent sockets no longer counted as "local keys": the keys check listed every file in the mounted construct home
.sshexcept a fixed non-key set, so per-bootagent.<pid>.sockproxy sockets accumulated there (60 on the dogfood host) and inflated the "Found N local keys" line. The filter is nowsshKeyNames, which skips directories,.pubcounterparts, known non-key files, and any*.sock(socket cleanup at the source is a tracked follow-up). - MicroVM daemon: skills toggle no longer silently misses the new mounts: a Sonnet peer review caught that
msbDaemonNeedsRecreatedid not consider skills mounts, so toggling skills on a running daemon left the change invisible until manual recreate. Fix: newconstruct.daemon.skills_hashlabel (see Added above). - Idle-watcher no longer races concurrent watchers or fresh
EnsureMsbDaemon: a Sonnet peer review caught thatStopMsbDaemonBestEffortdid not acquire the daemon flock, so two concurrent watchers could each independently decide "count==0" and race to stop the same daemon, AND a freshctinvocation'sEnsureMsbDaemoncould be torn down mid-flight. Fix:StopMsbDaemonBestEffortnow acquires the flock, re-checksLiveSessionCount()under the lock, then stops. A session that registered between the watcher's last tick and the stop is honored.
docs/DOGFOODING-1.16.3.mdP4.3 procedure corrected: the prepull check told the user to runct sys update, which is agent-update-inside-container and fails closed onbackend = "microvm"by design (exactly what the dogfood run hit). The deterministic path isct sys prepull(foreground pull loop, same log). Also documents thatct sys self-updateonly fires the prepull after an actual update — the "already on latest version" no-op returns before the spawn — and thatmsb image rmmatches by exact reference, not substring.docs/VMsv2.mdround 8 review provenance entry: records the Sonnet review follow-up (the two must-fix items + the nits we did not act on) so the next reviewer sees the trail.docs/VMsv2.mdP3.6 partial marker: the config template andct sys daemon statussession-count line are done; the fulldocs/CONFIGURATION.mdwrite is still pending (defer to a docs sweep).docs/VMsv2.mdround 7 entry: therequestLearnRootreturn-value split Sonnet originally flagged was traced to dead code (cleanProjectDiralready filters system roots upstream), reverted, and documented so a future maintainer does not "fix" away the no-opResolveDaemonMountsWithLearnedwrapper.
- MicroVM daemon: switching projects no longer recreates the sandbox: under
backend = "microvm", moving to a different project root destroyed and recreated the daemon microVM, wiping guest state (brew and apt installs) and re-running the multi-minute first boot on every switch. The Docker backend's multi-root daemon mounts are now ported to the microVM path: with[daemon] multi_paths_enabled = true, everymount_pathsroot is mounted under/workspaces/<hash>(full-path hash, so same-named repos cannot collide) and the daemon is reused for any directory inside the set; the sandbox is recreated only when the configured mount set itself changes (microsandbox mounts are create-time only, the SDK has no hot-add). A directory outside every configured root stops with an actionable error instead of a destructive recreate. In default single-path mode, subdirectories of the mounted root now reuse the daemon (previously even a subdirectory of the same project triggered a full recreate), and stale workspace labels are detected before reuse. Workdir mapping resolves symlinks on both sides, so paths reached through macOS/tmpvs/private/tmpor symlinked checkouts no longer spuriously count as outside the mount.
[runtime] enginemerged intobackend: the two config keys encoded one decision (engine picked the OCI binary, backend only switched container vs microVM, and engine was ignored under microvm), and the literal valuedockermeant different things on each key. The singlebackendkey now takesauto(default; detects container > podman > docker),container,podman,docker, ormicrovm; pinning a binary sets the priority order and detection still falls through when the pin is unavailable. Existing configs migrate automatically on first run with an in-place, comment-preserving rewrite: a pinnedengine = "podman"becomesbackend = "podman"on the same line,engine = "auto"is dropped, and an explicitbackendpin always wins over a leftover engine value; if the file cannot be rewritten, the same rules apply in memory for that session and the legacy value can never round-trip back throughSave(). The migration write is atomic (temp file plus rename), so a crash mid-write cannot truncate config.toml. One nuance: an old explicitbackend = "docker"with no engine now pins the Docker binary instead of auto-detecting; setbackend = "auto"to restore detection (documented in docs/CONFIGURATION.md).
- MicroVM users are pointed at the daemon setup: README, docs/CONFIGURATION.md, docs/ARCHITECTURE-DESIGN.md, and the config template now state the preferred microVM workflow: enable
[daemon] multi_paths_enabledwithmount_pathscovering your project roots so the daemon sandbox is created once and reused across projects, instead of being recreated (with a full guest re-initialization) on every directory switch.
- Rootless podman: agents crashed with
EACCES: permission deniedwriting under/home/construct:ct pi update --alland similar commands exec into the daemon as theconstructuser (uid 1000), but rootless podman's default user namespace maps container uid 1000 to a host subuid (e.g.100999) while container root maps to the real host user. The/home/constructbind mount is created host-side and owned by the host user, so the mappedconstructuser only had read access to it, surfacing asmkdir '/home/construct/.pi/agent/trust.json.lock'failures. The generateddocker-compose.override.ymlnow setsuserns_mode: keep-idfor rootless podman, which maps the host UID/GID directly into the container at the same numeric id instead of through the subuid range, soconstruct(uid 1000) aliases the host user and can write the mount it already owns.entrypoint.sh's root-phase ownership fix carried the opposite assumption (container root aliases the host user, so stay root and skip the chown); it now reads a newCONSTRUCT_USERNS_KEEPIDsignal to skip that fallback under keep-id and take the normal numeric-uid chown-then-drop-privileges path instead. The override cache key gained a version marker so every existingdocker-compose.override.ymlregenerates with the fix on next run; a daemon container already running under the old mapping keeps it until recreated (construct sys doctor --fix, which always stops and recreates the daemon), sinceuserns_modeonly takes effect at container creation.
- MicroVM hardware isolation engine (
backend = "microvm"): Construct now supports microVM hardware isolation powered by microsandbox as an opt-in runtime backend alongside OCI containers. Each sandbox runs within an isolated Linux guest kernel managed by hardware hypervisors (Apple Hypervisor.framework on macOS, KVM on Linux). The runtime layer standardizes lifecycle, command execution, streaming stdio, interactive PTY sessions, and image inspection through a modularBackendinterface (internal/runtime/backend.go). - Full guest-to-host bridge subsystem for microVMs: All Construct bridges communicate across the microVM boundary via
host.microsandbox.internal: token-authenticated clipboard text and image pasting, bidirectional host-exec path mapping, SSH agent proxying (with socket listeners placed on guest tmpfs to bypass VirtioFS socket creation constraints), Herdr status reporting, and loopback TCP relays (127.0.0.1:<port>->host.microsandbox.internal:<port>) for headless browser access to host development sites. - Resource sizing and persistent daemon management: Sandboxes configure 4 vCPUs and 4096 MiB RAM (
CPUs: 4, MemoryMiB: 4096) by default, preventing memory starvation or Linux OOM termination on heavy JS/TS agent heaps (Claude Code, Pi extensions, Codex, OpenCode). Sandboxes dynamically recreate when switching across host project roots, persist toolchains and package installations across stop/start cycles on the sandbox root disk, and validate backend readiness fail-closed without silent fallback. - MicroVM health diagnostics:
construct sys doctorincludes dedicated checks for the microVM backend (binary detection, daemon reachability, image state, and hypervisor virtualization support). rtk(Rust Token Killer) joins the default brew packages: token counting and context budgeting inside sandboxes, present on every fresh install.
- podman-compose parse failure on SELinux-labeled read-only mounts: the generated override emitted the global gitignore bind as
src:dst:ro:z; podman-compose's short-mount grammar accepts at most three colon-separated fields, so every setup on a podman host died withcould not parse mountbefore the container started. Mounts that already carry a mode flag now append the SELinux label as a comma-joined option (ro,z), which docker compose also accepts, and the override cache key gained amount_mode_syntaxfield so existingdocker-compose.override.ymlfiles regenerate instead of keeping the unparseable line. - Bun bootstrap failed on fresh containers (
unzip is required to install bun): the user-package installer reaches the Bun step before any brew formula is populated, and the Debian base image ships no unzip.unzipjoins the base image apt layer so the installer path is self-sufficient. - Config ownership doctor blocked scripted and agent sessions: rootless podman's user namespace periodically rewrites files under
~/.config/construct-clithrough the:zmounts, leaving them owned by a subordinate UID that blocks host-side writes. The doctor confirmed with a prompt that reads stdin (hung or consumed the piped stream in ssh batches and agent runs), and the migration fix path went straight to sudo, which cannot ask for a password without a terminal. Confirm prompts now gate onui.StdinIsTerminal(): non-interactive sessions run the rootless fix directly,runOwnershipFixtriespodman unshare chown -R 0:0before the sudo fallback, and sudo only inherits stdin on a real terminal.
construct sys shims: real PATH executables that route agents through the sandbox, for tools that spawn agent binaries directly: shell aliases only exist inside an interactive shell, so orchestrators, IDE extensions, and CI wrappers that resolve an agent binary on PATH or exec it without a shell (Paseo and similar harnesses spawningpi --mode rpc) never saw the aliases and ran the bare host binary.construct sys shims --install(default dir~/.local/bin) writes two executables per supported agent:<slug>execsconstruct <slug>with stdin/stdout passed through unchanged (JSONL RPC streams stay clean), andns-<slug>execs the real host binary directly, non-sandboxed (the ns- shell functions from the old alias system, now as files; the real binary is resolved on PATH while skipping the shim dir so it can never resolve to our own shim). Refuses to overwrite files it did not write (--forceoverrides), warns when the shim dir is off-PATH or another binary wins resolution, and--uninstallremoves only files carrying our marker.--remove-aliasesperforms a standalone cleanup of the legacy managed shell alias block for upgraders who do not want shims.- Host path arguments are staged into the sandbox for harness-driven runs: orchestrators pass absolute host paths as flag values (pi
--extensionand--mcp-configpoint at temp bridge files under/var/folders,--sessionat the host~/.pistore), and those paths do not exist inside the container (the sandbox home is the bind-mounted construct home; macOS Docker Desktop does not share/var/folders). Flagged values for known agents that resolve to existing host files under trusted roots (temp trees, the agent's host config dir, the caller's cwd) are copied to<construct home>/.construct-staging/<run-id>/(0700, 8 MB per file, 16 files per run) and rewritten to their/home/construct/.construct-staging/...paths, inengine.Prepareso the daemon exec, compose run, and msb paths all inherit it. Session files register a copy-back on Teardown so sandbox progress reaches the original host store. Values outside the allowlist are left untouched. Verified live with the exact Paseo argv: bridge extension loaded in-sandbox, sandbox-born session resumed, clean JSONL on stdout.
- The
construct sys aliasessystem is removed: shell aliases were invisible to non-shell callers, which is the gap shims close.sys shims --installmigrates existing setups by removing the managed# construct-cli aliases start/endblock from the shell rc (timestamped backup first; hand-written aliases and functions are never touched). For muscle memory and older instructions,construct sys aliases --uninstallstill works (deprecation note, removes the block, exit 0) while any other legacy aliases flag prints the replacement commands and exits 1. - All run-path status output now goes to stderr, keeping stdout reserved for the agent's own output: banners such as
Running in Construct daemon: [...], daemon startup, SSH/Herdr proxy notices, rebuild hints, migration chatter, first-run initialization, and image-build progress printed to stdout, which corrupts line-delimited JSON streams consumed by harnesses and other non-interactive callers. Newui.Info/InfoLn/InfoFhelpers carry these messages oninternal/agent(engine, runner, msb),internal/migration,internal/runtime,internal/env,internal/config, andinternal/network; interactive output is visually unchanged because terminals merge both streams. The interactive attach prompt and explicit CLI output (construct agents, help) intentionally remain on stdout. Verified warm:pi --mode rpcthrough an installed shim returns a pure JSONL stream with zero non-JSON stdout lines; cold-start output (first-run init, image build) also lands on stderr.
- Host exec bridge now propagates the agent's working directory to host binaries: when an agent invokes an allowlisted host binary (
host_binaries, e.g.wicket) from inside the sandbox, the bridge previously ran it in the daemon's cwd. So cwd-aware CLIs likewicket wp(which detect the Docker Compose project by walking up from cwd for a compose file, with no--projectflag) resolved the wrong project and failed withno services found. The shim (internal/templates/construct-host-exec) now sends the container$PWDas acwdfield on the/execpayload;internal/hostexec/server.gotranslates it to the matching host path via the project mount (${PWD}:${CONSTRUCT_PROJECT_PATH}), validates the result stays at or under the host project root (rejecting../escapes and arbitrary host dirs like/etc), and sets it as the child's working directory. Empty, unset, or out-of-mount cwd falls back to inheriting the daemon cwd (the previous behavior), so the change is backward compatible.internal/agent/engine.gopassesruntime.GetProjectMountPath()(container root) ande.cwd(host root) toStartServer. After this, an agent cancdinto a client project and runwicket wp/wicket dockerand have them target that project on the host. Security: cwd is constrained to the project mount; anything outside the host project root is rejected, which adds nothing the agent could not already do (it can alreadycdthere and the binary already runs as the host user). v1 limitation: only the project mount is translated, not the home mount (/home/construct); a cwd there falls back to inheriting. Requiresconstruct buildto bake the updated shim into the image. Documented indocs/HOST-EXEC.md.
- Host exec cwd propagation now covers daemon
/workspacesmounts, not just the project mount: 1.14.1 translated the agent's container cwd only against the primary project mount (/projects/<name>), so an agent whose cwd sat under a daemon multi-path mount (/workspaces/<sha256[:8]>, one perdaemon.mount_pathsentry) could not be translated andwicket wpfell back to the daemon cwd, failing withno services found.StartServernow takes a list ofPathMap{Container,Host};internal/agent/engine.gopasses the project mount plus everyruntime.ResolveDaemonMountsmount, andresolveHostCwdtries each map while still rejecting anything that escapes a mount's host root. Empty or unknown cwd still falls back to inheriting the daemon cwd. Backward compatible; the home mount (/home/construct) remains untranslated. Requiresconstruct buildto bake the updated shim. Documented indocs/HOST-EXEC.md.
- Host loopback forwarding lets headless browsers reach host dev sites: agent-browser's Chromium runs inside the sandbox, but Chromium hardcodes
localhostand*.localhostto127.0.0.1(RFC 6761), bypassing/etc/hosts, DNS,dnsmasq, and--host-resolver-rules. So a headless browser could not reach host dev servers likehttp://hyperpress.localhost, even though non-browser tools (curl, git, MCP) reached them fine. The fix is blind TCP relays on the container's127.0.0.1that forward tohost.docker.internal, launched byentrypoint.sh(socat, next to the SSH bridge). Blind relay preserves the HTTP Host header and TLS SNI, so host vhost routers (valet, Hyperpress) and certs see the real hostname. Configured via[sandbox] host_loopback_ports(list of ints, default[80, 443], same port both sides). EmittingCONSTRUCT_LOOPBACK_PORTSintodocker-compose.override.ymlalso addscap_add: NET_BIND_SERVICE(consolidated with strict-modeNET_ADMINinto onecap_add:block) so the non-root construct user's socat can bind privileged ports. The port list is hash-tracked inoverrideInputs, so changing it regenerates the override; an empty list disables the feature and drops the cap. Platform caveat: on Linux the host-gateway is the bridge IP, so host services must bind0.0.0.0/bridge (not127.0.0.1-only) to be reachable; macOS host-gateway already routes to host127.0.0.1. - Terminal identity markers now forward into the sandbox:
KITTY_WINDOW_ID,GHOSTTY_RESOURCES_DIR, andTERM_PROGRAMare passed into the container as-eflags on both launch paths (directcompose runand the daemoncompose run -d, sodocker execsessions inherit them) so in-container TUIs and pi extensions can detect the outer terminal and do the right thing (kitty-graphics inline image rendering, etc.).TERMitself is intentionally NOT forwarded by default to avoid terminfo mismatches (forwardingxterm-kittyinto a container without kitty-terminfo breaks ncurses apps like less/vim/btop); the identity vars are enough for detection. Users who needTERMcan add it toenv_passthroughand installncurses-term/kitty-terminfoin the image.
construct buildno longer fails on the socat file cap: the Dockerfile ransetcap cap_net_bind_service+ep /usr/bin/socatat build time, but BuildKit's default sandbox blocks file-capability writes duringdocker buildand aborts withInvalid file 'setcap' for capability operation(granting it needs thesecurity.insecureentitlement, whichdocker compose builddoes not enable by default). The file cap is now applied at runtime:entrypoint.shruns the samesetcapas root in its startup block, before thegosudrop to the construct user. It is idempotent and best-effort, and the cap persists on the overlay for the container's lifetime, so the non-root socat still binds80/443via the file cap plus theNET_BIND_SERVICEbounding set (both still required).libcap2-bin(which provides thesetcapbinary) stays installed in the image.
- Host qmd model cache is now reused inside the sandbox: when the host has a qmd GGUF model cache at
$XDG_CACHE_HOME/qmd/models(or~/.cache/qmd/modelsby default), Construct bind-mounts it into the container at/home/construct/.cache/qmd/modelsso the qmd semantic-search backend reuses the already-downloaded models (~1.5 GB) instead of re-fetching them on every container recreate. The mount is read-write so models qmd fetches lazily later (the reranker and query-expansion GGUFs pull on firstqmd query) write back to the shared host cache and become one-time. It is added only when the host directory exists (no config flag, no effect when absent), is resolved the same way qmd resolves its own cache ($XDG_CACHE_HOMEfirst, then$HOME/.cache), and is hash-tracked inoverrideInputssodocker-compose.override.ymlregenerates when the directory appears or disappears. Mirrors the existing conditional gitignore mount. Documented indocs/ARCHITECTURE-DESIGN.mdanddocs/CONFIGURATION.md;AGENTS.mdgains a contributor note on the conditional-mount pattern for future additions.
construct sys daemon restartno longer fails on Docker Desktop: the daemon could fail to restart with a bare "exit status 1" that swallowed docker's actual error. The root cause was two-fold.CleanupExitedContainerran a plaindocker rm, which races on macOS where a freshly stopped container can briefly resist removal while Docker Desktop tears down host bind-mounts (extra bind-mounts, including the new qmd cache mount, widen the window). AndStart()/Restart()downgraded that cleanup failure to a warning and then randocker compose run --name, which hit a name conflict with the not-yet-removed container. Fixes:CleanupExitedContainernow usesdocker rm -f(force, safe because the daemon has no dependent containers); cleanup failure inStart()now aborts instead of warn-and-continue; andRestart()no longer duplicates the exited-container cleanup, delegating it toStart()as the single owner of that logic. End-to-end verified:construct sys daemon restartexits 0 and the qmd mount survives the restart.- Daemon start and cleanup errors now surface docker's stderr:
Start(),StopContainer, andCleanupExitedContainerswitched fromcmd.Run()tocmd.CombinedOutput()and include the captured output in the error, so a failed daemon start prints docker/compose's real reason instead of an opaque "exit status 1". This is what made the restart failure above diagnosable instead of a guessing game.
- Host exec shim no longer deadlocks on open-but-empty stdin:
construct-host-execblocked forever when launched with stdin as an open pipe that never sent data and never closed (no EOF), which is exactly the condition created by non-interactive launchers likepi-unified-execthat hold stdin open for the session lifetime. The old barebase64read sat waiting for bytes or EOF that never arrived, so every proxied host binary (e.g.wicket config servers --json) hung with zero output under such launchers while working fine in a human's interactive terminal. The fix is a hybrid read: a non-blockingread -t 0peek first (returns success only when data or EOF is immediately available), and if nothing is pending the read is skipped entirely, the common case for flag-only CLIs. Only when data or EOF is actually waiting does the shim consume stdin, bounded byhead -c 1048576(1 MiB cap, forces EOF after N bytes or on real EOF sobase64always flushes and closes) andtimeout 5as defense-in-depth against slow trickle feeds. If either bound fires, the shim now emits a stderr warning attributing the truncation to itself instead of surfacing it later as a confusing bridge-side parse error. The bridge contract (stdin ships up front, base64-encoded, live interactive input still unsupported by design) is unchanged. Regression tests cover the open-empty pipe case (asserts sub-3s return, was an infinite hang) and the over-cap truncation warning.
- Pi Extension Package Support: New
[pi]config section lets you install pi coding-agent extensions through Construct's generated setup script instead of hand-runningpi install. Sources use pi's own install syntax (npm:name,git:github.com/user/repo,url, or a local path) and are passed verbatim topi install, which manages~/.pi/agent/npmandsettings.jsonatomically and is idempotent, so re-runningct sys packages --installis safe. The block runs after the[npm]step sopiis already on PATH, and it is guarded bycommand -v pi: if pi is absent the configured extensions are skipped with a warning rather than aborting the whole install. Each package is isolated (|| echo), so one bad extension does not take down the rest.internal/templates/packages.tomlships the section empty with commented examples so you opt in by uncommenting. Tests cover TOML parsing (table-driven), script generation, verbatim source passing, the empty-config no-op, and the missing-binary guard branch.
- Script generators now use
strings.Builder:GenerateInstallScriptandGenerateTopgradeConfig(ininternal/config/packages.go) stopped concatenating with+=in loops and switched tostrings.Builder, withfmt.Fprintffor the quoted-list entries. Same generated output, less allocation churn. Side benefit: theconfiglocal inGenerateTopgradeConfigno longer shadows the package name.
- Herdr Integration Bridge: When
constructis launched from a Herdr pane, per-agent Herdr integrations now fire inside the container, producing the turn-level idle/working status panel natively (green checkmark when the agent is ready, spinner when working). Previously the agent ran in an isolated container PID namespace invisible to Herdr's host-side foreground-job detection, leaving a permanent spinner.- Integration file sync (
internal/agent/integration_sync.go): mirrors host-side integration files into the construct home per agent slug. Currently syncsherdr-agent-state.tsfor pi from$PI_CODING_AGENT_DIR/agent/extensions/(or~/.pi/agent/extensions/) to the container's~/.pi/agent/extensions/. SHA-256 content compare: copies only when missing or different, so Herdr-side updates propagate on the next run; identical files are skipped (mtime preserved). Per-agent registry; other agents can be added later. Best-effort; never blocks the run. - Herdr socket bridge (
internal/agent/herdr_bridge.go): the host Herdr API socket is AF_UNIX and unreachable from a container via bind-mount (Docker Desktop surfaces a regular file, not a live socket —Connection refused). A host TCP listener proxies each connection to the unix socket, and a per-session in-containersocat(runtime-launched viadocker exec, no image rebuild) bridges back tohost.docker.internal:<port>. Mirrors the existing SSH agent bridge: deterministic port band48600-58599(no SSH overlap) with ephemeral fallback, macOS binds127.0.0.1/ Linux0.0.0.0, per-PID socket/tmp/herdr-agent.<pid>.sock, cleaned up inTeardown(). - Env forwarding: injects
HERDR_ENV=1,HERDR_PANE_ID(forwarded verbatim from the host pane shell), andHERDR_SOCKET_PATH(repointed at the in-container proxy socket) into the exec env. These three are the gate Herdr's integrations check (enabled()returns false without all three). Active on the daemon and attach paths;runNewContainer(compose run with no daemon) is intentionally not covered since the in-container socat does not exist there.
- Integration file sync (
- Seccomp Relaxation (
disable_seccomp): New[sandbox]config option that emitssecurity_opt: [seccomp:unconfined]in the generateddocker-compose.override.yml, enabling headless browser automation inside the container. Docker's default seccomp filter blocks syscalls Chrome/Chromium's multi-process CDP backend needs (clone3with namespace flags,seccomp(2)BPF install,ptrace), causing every persistent browser launch (agent-browser, Playwright, CDP extensions) to die withTrace/breakpoint trap. Default-off; opt in with[sandbox] disable_seccomp = true. The toggle is hash-tracked so flipping it regenerates the override, andconstruct sys doctorsurfaces when it is enabled (with a security note). Requiresconstruct build+ container restart to take effect. Seedocs/SECURITY.mdfor the tradeoff (removes a kernel-level syscall-restriction layer).
- Host Exec Bridge (
host_binaries): New[sandbox]config option that lets the agent invoke selected host-only binaries from inside the container, where they actually execute on the host machine (not in the sandbox). The agent sees them on PATH and calls them normally; a shim transparently proxies each call to a host-side bridge that runs the real binary as your host user and streams stdout/stderr back with exit codes preserved.- Config:
[sandbox] host_binaries = ["wicket"]. Off when empty (no bridge starts, zero attack surface). - Security model: each listed binary runs on the host with full container-controlled argv. The bridge is token-gated (per-session 32-byte bearer,
X-Construct-Exec-Token), allowlist-pinned (unknownargv[0]fails closed with 403), and resolves each binary to an absolute host path at startup (no per-requestexec.LookPath, no PATH-poisoning). Only list binaries you trust with container-controlled argv — declaring e.g.dockergrants effective host root to the agent. A startup banner (⚠ host exec enabled: …) confirms when active; seedocs/HOST-EXEC.mdfor the full threat model. - No interactive TTY by design: there is no controlling terminal; pipe stdin (one-shot) works, interactive prompts do not. Pass
--no-interactive/--json/--yesflags where the listed binary offers them. Future TTY support, if ever needed, is an HTTP-upgrade transport on the same bridge (not a socat rebuild). - Daemon-safe: symlinks are reconciled host-side in
Prepare()(nodocker exec), so toggling the list takes effect on the nextconstructinvocation without a daemon restart. Requiresconstruct buildafter first enabling (the shim is baked into the image). - Per-call timeout: 30-minute hard cap (override via
CONSTRUCT_HOST_EXEC_TIMEOUT); bridge-initiated kills surface as exit code124(thetimeout(1)convention). - Audit log: every invocation is recorded at
~/.config/construct-cli/logs/host_exec.log(timestamp, argv, resolved path, exit code, duration).construct sys doctorwarns on misconfig (listed binary missing on host PATH, stale shim manifest, shim not yet baked into the image).
- Config:
- SSH Identity Pinning (
ssh_pin_identities): New[sandbox]config option that pins one SSH identity per host to avoidToo many authentication failureswhen the forwarded agent holds many keys. Two entry forms:"host=keyname"(simple) and"alias=hostname=keyname"(multi-account on same service). Pinned identities are serialized intoCONSTRUCT_SSH_PIN_IDENTITIESand consumed byensure_ssh_config()in the container entrypoint, which emitsIdentitiesOnly yes+ the named key for each configured host. - Per-Session SSH Proxy Sockets: Each
constructprocess now uses its own socat socket (/home/construct/.ssh/agent.<pid>.sock) rather than a shared/home/construct/.ssh/agent.sock. Concurrent sessions sharing one daemon no longer overwrite each other's agent proxy.Teardown()cleans up only the calling session's socket. - SSH Bridge Regression Tests: Table-driven tests for
sshPinIdentitiesEnv(10 cases including malformed-entry guards),sshProxySockForPID(distinct PIDs, stable same PID, exact path), bridge no-agent guard, full proxy integration, and Bitwarden vault lock/unlock recycle case. - Entrypoint
ensure_ssh_configBash Tests: Newinternal/templates/entrypoint_ssh_config_test.goextracts and executes the realensure_ssh_configshell function in an isolatedHOME. Seven cases: no hardcodedIdentityAgent, physical-keys-only, agent pin with.pub, alias three-field form, physical-key fallback, pin skipped when key missing, opt-out (# construct-managed: false) respected.
- SSH Agent Forwarding in Container:
ensure_ssh_config()no longer emits phantomIdentityFilepaths for keys that do not exist on disk, eliminating false "no SSH key" reports from agents. Hardcoded~/.ssh/defaultand~/.ssh/personalare only emitted when the files are actually present. IdentityAgentOverride Removed: TheHost *block no longer writesIdentityAgent ~/.ssh/agent.sock, which was overriding the per-sessionSSH_AUTH_SOCKenv var and routing all agent requests to whichever session last wrote that socket. SSH now usesSSH_AUTH_SOCKdirectly, injected per-session by the engine.- Error Context on SSH Proxy Helpers (Go Mistakes #49): Both
ensureDaemonSSHProxyandwaitForDaemonSSHProxynow wrap errors with%w, including container name, socket path, and port, enablingerrors.Is/errors.Asunwrapping and actionable messages in multi-session scenarios.
- Pi Update No Longer Updates Extensions: Pi changed the bare
pi updateto update only pi (self), withpi update --allrequired to update pi and its extensions together. Construct's system update (ct sys update) was still calling barepi update, so pi extensions were silently no longer updated on each run. All three invocation sites now usepi update --all: the generated topgrade config (packages.go), the statictopgrade.tomltemplate, and the manualupdate-all.shfallback.
- Worktrunk: Removed
worktrunkfrom the default Cargo install list (packages.toml). Upstreamworktrunk 0.59.0fails to build from a registry tarball (cargo install) because itsvergen-gitclbuild script cannot computeVERGEN_GIT_DESCRIBEoutside a git worktree, andsrc/cli/mod.rsuses a hard compile-timeenv!("VERGEN_GIT_DESCRIBE"). The Topgrade/Cargo update step aborted on every update run. The historical 0.11.1 addition entry is retained for accuracy.
- SSH Agent Reachability Check in
sys doctor: The existing "SSH Agent" check only verifiedSSH_AUTH_SOCKwas set, which a stale/recycled socket (e.g. Bitwarden/1Password after lock) passes while every in-containerssh/gitop fails silently. The check now probes the agent directly viassh-add -land reports reachable (with key count), reachable-but-no-keys, not-reachable, or unknown (ssh-add missing). Extracted into a unit-testedcheckSSHAgenthelper.
- Concurrent Setup Deadlock: Two
docker compose runsetups running at once (e.g. a user retrying because setup looked stuck) share the same home bind-mount and both write into npm's shared globalnode_modules+ cache, deadlocking on npm's cache/lock and hanging indefinitely. Setup now takes a non-blocking exclusiveflockon~/.config/construct-cli/setup.lockbefore spawning the compose run; a second instance refuses to start and tells the user to wait. The lock auto-releases on process exit, so no manual cleanup is needed after a crash. - Redundant npm Reinstalls During Setup: Every
npm install -gin the generated setup script used--force, re-fetching and re-linking all global packages on every setup run. This made setup slow enough to look stuck (the trigger for the retry that caused the deadlock above).--forceis dropped from the setup path so npm skips packages already at the target version.update-all.shstill uses--forcefor explicit@latestupgrades (intentional). - SSH Agent Bridge Silently Failed on Stale Port: The in-container
socatSSH-agent proxy (started byentrypoint.shat container creation) baked the host bridge port into its argv. The host bridge (StartSSHBridge) bound a random ephemeral port that changed across CLI invocations, so the stalesocatkept pointing at a dead host port and everyssh/gitop failed with "communication with agent failed". The v1.8.15 "restartsocaton each exec" fix did not actually work:ensureDaemonSSHProxybackgroundssocat(returns near-instantly, almost never errors) and both exec sites discarded the result ofwaitForDaemonSSHProxy(gated onif err == nil). The liveness probe wastest -S(socket file exists), which a leftover socket or stalesocatpasses. Now both exec sites check and log both errors, and the probe is a realUNIX-CONNECT(socket actually accepting connections). - Topgrade Brew Step Crash on Linux: The
homebrew/casktap (auto-tapped underHOMEBREW_NO_INSTALL_FROM_APImode) breaksbrew upgradeon Linux because casks use arch-conditionalsha256 arm:/intel:that resolves to nil on non-macOS systems (e.g.Casks/0/0-ad), aborting the whole update run and stalling all formula upgrades. Bothupdate-all.shandentrypoint.shnow defensively untaphomebrew/caskon Linux (idempotent). Casks are macOS-only and non-functional on the Linux box.
- Deterministic SSH Bridge Port per Box:
StartSSHBridgenow derives a stable TCP port from the box identity (hash of the container name) instead of an ephemeral random port, with fallback to ephemeral on bind failure. The same box now maps to the same host port across invocations, so a stalesocatbaked at container creation keeps pointing at the right port instead of aging out. Band sits below the Linux ephemeral range (32768+) to reduce OS collisions. Correctness is still guaranteed by the per-execsocatrestart regardless of port strategy.
- Non-interactive Container Exec: New
construct sys exec -- <command>command allows running a single command inside a running Construct container without attaching to an interactive shell. Designed for LLM agents operating in headless environments that need to execute commands inside the container and capture output. Streams stdout/stderr separately to the host, returns the container process exit code. Supports both daemon and CWD-scoped containers. Requires a running container (start withconstruct sys shellorconstruct sys daemon start). - Daemon Name Constant: Canonical
DaemonNameconstant ininternal/constants/constants.goreplaces scattered string literals across agent engine and sys packages. - Container Naming Export:
CwdContainerName()moved frominternal/agent(unexported) tointernal/runtime(exported) for cross-package reuse. - Non-interactive Exec Primitive:
ExecNonInteractiveStream()ininternal/runtime/runtime.goexecutes commands in running containers without TTY allocation, streaming stdout/stderr separately, returning real exit codes.
MapDaemonWorkdirandReadKeyringEnvexported from agent package for reuse bysys exec.- Smart Migration Rebuilds: Container image rebuilds are now gated by per-template hash tracking. Version bumps that only change Go code (no template changes) skip the rebuild entirely, reducing update time from ~2 minutes to ~2 seconds. Templates are classified into tiers: image-baked (Dockerfile, entrypoint.sh, etc.) trigger a full rebuild; runtime-only (docker-compose.yml, agent-patch.sh) trigger a deferred restart; no changes means no rebuild.
- Cross-Platform SSH Agent Bridge: Replaced macOS-only SSH agent forwarding with a TCP bridge that works on both macOS and Linux. On macOS the bridge binds
127.0.0.1(Docker Desktop routes it); on Linux it binds0.0.0.0so containers reach it viahost.docker.internal. Removed directSSH_AUTH_SOCKsocket mounts and permission-fixing logic from entrypoint and compose overrides. - Dynamic SSH Agent Socket Re-reading: The TCP bridge now re-reads
SSH_AUTH_SOCKper connection, with up to 3 retry attempts (100ms backoff). Handles agents like Bitwarden that recycle socket paths on vault lock/unlock. - Daemon SSH Proxy Restart: Running containers now get their socat proxy restarted with the current bridge port on each
exec, preventing stale-port failures across sessions.
- NPM Package Setup Failures: Added
--forceflag to global npm package installs and upgrades during construct provisioning. Prevents setup crashes caused by pre-existing symlink conflicts (e.g.EEXISTconflicts during@kilocode/cliinstallation) and cascadingtar TAR_ENTRY_ERROR ENOENTextraction errors (which blocked the installation of thepipackage). - SSH Agent Proxy Leak: Added
pkillcleanup commands in container provisioning and execution engines to terminate stale backgroundsocatsocket listeners before binding new instances, preventing orphaned process accumulation and routing failures across sessions.
- Global Gitignore Mount: User's global gitignore file is now automatically mounted (read-only) into the container at
/home/construct/.config/git/ignore. Detects the file from four locations in priority order:git config core.excludesFile,$XDG_CONFIG_HOME/git/ignore,~/.gitignore, and~/.gitignore_global. Tilde paths and spaces in paths are handled correctly. Mount is included in the override hash for proper cache invalidation.
- CWD-Derived Container Naming: Each working directory now gets its own container (
construct-cli-<sha256[:8]>) instead of a shared singleton. Running agents from multiple terminals with different working directories no longer conflicts with "Container 'construct-cli' is already running." Same directory always hashes to the same container name, preserving attach semantics.
- Daemon Killed by
sys doctor --fix:cleanupAgentContainerprefix-matchedconstruct-cli-daemonand killed it;recreateDaemonContainerthen saw it missing and no-op'd. Daemon is now excluded from session container cleanup. - Stopped Containers Returned by Network Manager:
runningSessionContainersuseddocker ps -aq(all states), causing spurious UFW rule warnings on stopped containers. Now filters by running state. - Legacy Singleton Missed by Migration:
collectSessionContainersonly discovered CWD-hash containers, missing pre-upgradeconstruct-clisingleton. Now includes exact-match discovery for the legacy name. - Stale Error Message:
sys doctor --fixsuggestion referenced olddocker rm -f construct-cliinstead of prefix-based cleanup. - Keyring Env Path Hardcoding:
readKeyringEnvusedos.UserHomeDir()instead ofconfig.GetConfigDir()for the keyring env file path.
- Container Discovery: All consumers of the static
"construct-cli"container name (doctor.go,migration.go,network/manager.go,reset-environment.sh) now discover containers by prefix"construct-cli-"viaruntime.ListContainersByPrefix(). - Architecture Docs: Updated
ARCHITECTURE-DESIGN.mdSections 4, 9.3, and 11.2.1 for the new naming pattern.
- Antigravity Update Integration: Added
agy updateintegration to the dynamic Topgrade generator (packages.go),topgrade.tomltemplate, and the manual system update fallback script (update-all.sh).
- Yolo Configuration for agy: Fixed yolo settings (
yolo_allandyolo_agents) to correctly apply the--dangerously-skip-permissionsflag when initializing theagyagent. - Agent Credential Persistence: Added
gnome-keyring,libsecret-1-0, anddbus-x11to the container image, with automatic daemon startup in the entrypoint. Agents (e.g.,agy) that rely on the OS keyring for OAuth tokens now persist credentials across container restarts. Previously, every session required a fresh login. - Keyring Daemon Startup: Fixed
gnome-keyring-daemoninvocation in the entrypoint. The--startand--unlockflags are mutually exclusive and caused the daemon to silently fail, leaving the keyring locked. Changed to--unlock --components=secretswhich both starts the daemon and unlocks the login keyring with a blank password. - Keyring Env Vars Not Reaching Agents:
docker execruns agents directly (no shell), so.bashrc/.profileare never sourced andGNOME_KEYRING_CONTROL/DBUS_SESSION_BUS_ADDRESSwere invisible to agent binaries. agy's keyring auth timed out after 1s, fell back to browser OAuth, and hung. Fixed by having the entrypoint write these vars to~/.construct-keyring-env, which the Go CLI reads from the host-side bind mount and injects via-eflags on everydocker exec.
- Replaced Gemini CLI with Antigravity CLI: Replaced
geminiagent (Google Gemini CLI, npm-installed) withagyagent (Google Antigravity CLI, curl-installed fromhttps://antigravity.google/cli/install.sh). Binary lands at~/.local/bin/agy. Removed Gemini-specific clipboard paste wrapper (~220 lines of Python PTY code). RenamedGEMINI_API_KEYtoANTIGRAVITY_API_KEYthroughout source, templates, and docs. Updated agent registration, memory paths, constants, env passthrough, runtime candidates, wayland/yolo flags, help text, aliases, verification loops, and all documentation.
- Yolo Mode Ignored on Cold-Start: Config values (
yolo_all,yolo_agents) fromconfig.tomlwere not applied when running agents via the cold-start (non-daemon) path. Root cause:Execute()applied yolo flags to a local variable, butrunNewContainer()discarded itsargsparameter (_) and reade.argsdirectly from the struct. The daemon path worked because it passed yolo-applied args directly. Fixed by writing yolo results back toe.argsand removing the dead parameter fromrunNewContainer().
- Global AGENTS.md Symlinks Leaked onto Host: Removed creation of
AGENTS.md,CLAUDE.md, andGEMINI.mdsymlinks in/workspaces/and/projects/. These directories are host bind mounts, so symlinks pointing to the container-internal/home/construct/AGENTS.mdleaked onto the host filesystem as dangling links. Symlink aliases are now created only inside/home/construct/(container-internal). Existing dangling symlinks on the host must be cleaned up manually.
- AGENTS.md Symlink Scope: Fixed symlinks being created at every directory level (including project subdirectories), which overwrote pre-existing AGENTS.md files. Symlinks are now created only at
/workspaces/,/workspaces/<hash>/, and/projects/. Pre-existing real files are never overwritten. Now also createsCLAUDE.mdandGEMINI.mdsymlinks alongsideAGENTS.md, all pointing to the global rules file.
- Global AGENTS.md Symlinks: Automatically creates symlinks to the global
AGENTS.mdrules file in common mount points (/workspaces/,/projects/) and their subdirectories. This ensures agents can easily discover global instructions regardless of the project's mount path or random daemon hash.
- ARM64 Setup & agent-browser: Fixed
agent-browserinstallation failure on ARM64 Linux by implementing an automatic wrapper that uses the system Chromium (/usr/bin/chromium). - Resilient Package Installation: Added failure-safe command execution (
cmd || echo ...) ininstall_user_packages.shto prevent a single tool failure from aborting the entire setup. - Sudo Detection: Improved
SUDO_AVAILABLEcheck to usesudo -n apt-get --version, resolving issues in restricted sudoers environments wheretruewas not permitted. - Multimodal Clipboard Regression: Resolved image pasting failures in Gemini and Codex agents.
- macOS Networking: Fixed
127.0.0.1hardcoding in the clipboard server; now correctly useshost.docker.internal(or configuredclipboard_host), making the host clipboard reachable from the container. - Ghostty / Bracketed Paste Support: Added PTY interception for
\x1b[200~sequences. PTY wrappers now consume bracketed paste content and inject image paths directly, bypassing terminal protocol mismatches. - Daemon Patching: Restored
runAgentPatchInDaemonlogic to ensure shims, wrappers, and Xvfb are correctly initialized in persistent daemon containers. - Headless X11 (Xvfb): Moved Xvfb startup to the agent-patch script to ensure a valid
DISPLAYis always available for agents that expect a real display. - Gemini @-prefix: Ensured Gemini uses the required
@pathinjection format while Codex uses raw paths.
- macOS Networking: Fixed
- Agent Update Hardening: Added defensive
command -vchecks for Claude and Pi update commands in Topgrade configuration, preventing errors in environments where these agents are not installed.
- Official Goose CLI Installer: Switched to the official Goose installation script (
github.com/aaif-goose/goose) for more reliable setup. - Updated OpenCode Installer: Updated the OpenCode post-install command to use the canonical
curl -fsSL https://opencode.ai/install | bash. - Pi Update Integration: Integrated
pi updateinto the primary Topgrade update path to ensure Pi and its internal packages are kept current duringct sys update. - Clean Brew Installation Logs: Optimized the Homebrew installation script to check for existing packages before running
brew install, replacing noisy "already installed" warnings with clean confirmation messages.
- Fast Agent Startup (Patch Marker): Implemented a versioned marker-based guard (
~/.construct_patched) to skip redundant agent clipboard patching on startup. This significantly reduces latency when entering a shell or launching an agent in a warm daemon container. Patching is now only re-triggered after Construct version changes or package installations.
- Daemon shell with no args:
execViaDaemonnow defaults to the configured shell (sandbox.shellor/bin/bash) when invoked without arguments, matching the existing behavior ofexecInRunningContainer. Previously,ct sys shellpassed an empty command todocker exec, causing "requires at least 2 arguments" error.
- Root exec in daemon containers:
resolveExecUserForRunningContainer()now returns"construct"on all code paths instead of"". Previously,docker exec -itinto the daemon ran agents as root (sinceUSER constructis commented out in the Dockerfile), causing Claude CLI to reject--dangerously-skip-permissions. Added uid==0 guard andUsesUserNamespaceRemap()check. - Pre-existing test failure on uid=0 CI:
TestAppendExecUserRunFlagsnow correctly handles root environments by expecting no--userflag when uid=0. - Missing daemon launch messages: Restored
"Running in Construct daemon: [args]","Entering Construct daemon shell...","✓ Started SSH Agent proxy (daemon)", and exit code 126/127 PATH hints that were lost in the v1.8.0 refactor.
- Claude install moved to packages.toml: Claude Code installation moved from hardcoded
packages.goto[post_install]section inpackages.toml, consistent with droid/opencode pattern. - Pi self-update in update routine: Added
pi updateto bothupdate-all.shmanual fallback and topgrade[commands]config.
- Dead code cleanup: Removed unused
containerHasUIDEntryFnvariable, deadexecUserForAgentExecfunction (never called from production), and stale test mocks that referenced removed behavior.
- Runtime Engine Extraction: Extracted ~1000 lines of monolithic container orchestration from
internal/agent/runner.gointo a dedicatedRuntimeEnginein newinternal/agent/engine.go.runner.gonow delegates toengine.Prepare()andengine.Execute(), centralizing daemon detection, clipboard server, SSH bridge, login forwarding, environment assembly, and container state handling. - Security Session Interface: Introduced a new
Sessioninterface ininternal/security/session_deep.gowithnoOpSession(disabled) andsecureSession(active) implementations. Replaces directSessionManagercoupling throughout the codebase with a clean, testable abstraction. - SecretShield Deep Module: Created
internal/security/shield.goas a deep module encapsulating secret detection and redaction into a single atomicProtect()operation.integration.gonow delegates scanning toSecretShieldinstead of orchestratingScannerdirectly. - Runner Integration Refactor:
internal/security/runner_integration.gocompletely rewritten around theSessioninterface. Uses the newsecurity.Open()factory and delegates env masking tosess.MaskEnv(). Eliminates redundantEnvMaskerinstantiation at every call site. - Scanner JSON Serialization: Replaced hand-rolled string-buffer JSON generation in
internal/security/scanner.gowith properencoding/jsonMarshalIndent/Unmarshalfor manifest and redaction index I/O. - RiPGrep Path Resolution: Changed hardcoded
/usr/bin/rgstat toexec.LookPath("rg")for cross-distro compatibility. - Session Manager Cleanup Removal: Removed
Manager.Cleanup()andisProcessAliveorphan-reaping logic frominternal/security/session.go. Session lifecycle is now managed explicitly through theSessioninterface. - Workspace Type Override: Added
CONSTRUCT_SECURITY_WORKSPACE_TYPEenvironment variable andWorkspaceTypeNonefor testability.DetectWorkspaceType()now respects the override. - OverlayFS Mount Fix: Corrected overlayfs mount options to use
lowerdir=<lower>,upperdir=<upper>,workdir=<work>instead of the erroneouslowerdir=<lower>:<upper>,upperdir=<upper>,workdir=<work>that duplicated the upper layer in the lower stack. - Setup PATH Construction:
runSetupnow constructsPATHandCONSTRUCT_PATHdirectly viaenv.BuildConstructPath()instead of the removedapplyConstructPath()helper.
- Clipboard Server Stop: Added
Stop()method tointernal/clipboard/server.gofor clean listener shutdown. - Security Session Tests: Added
internal/security/session_test.gowith regression coverage for bothnoOpSession(disabled) andsecureSession(enabled withCONSTRUCT_SECURITY_WORKSPACE_TYPE=none) deep interface behavior.
- Nil Status Guards: Added nil-pointer checks in
internal/security/integration.gobefore callingsm.status.IsEnabled(). - CI Lint Toolchain Pin: Bumped
golangci-lintpin fromv2.11.4tov2.12.2in build and release workflows; aligned local Makefile CI pin to2.12.2.
- ~29 Helper Functions from runner.go: Removed
collectForwardedEnv,buildRunFlags,shouldEnableLoginForward,applyYoloArgs,applyConstructPath,execUserForAgentExec,appendExecUserRunFlags,resolveExecUserForRunningContainer,shouldEnableYolo,yoloFlagForAgent,readLoginBridgePorts,parsePorts,formatPorts,mapDaemonWorkdir,warnDaemonMountFallback,getEffectiveCwd,execViaDaemon,runAgentPatchInDaemon,buildDaemonExecEnv,execInRunningContainer,startDaemonSSHBridge,ensureDaemonSSHProxy,waitForDaemonSSHProxy,checkDaemonSSHProxy,startDaemonBackground,waitForDaemon,ensureAgentRuntimeDirs,appendAgentSpecificRunFlags,appendAgentSpecificDaemonEnv, andappendAgentSpecificExecEnv. All behavior preserved inRuntimeEngine.
- Pi Coding Agent Package: Switched from
@mariozechner/pi-coding-agentto@earendil-works/pi-coding-agentin default npm packages. - Gemini CLI Distribution: Moved
gemini-clifrom Homebrew to npm (@google/gemini-cli) for consistent cross-platform installation. - OpenCode Install Source: Removed
opencodefrom default Homebrew packages; added automatic OpenCode installation via official installer in[post_install]commands.
- Oh My Pi Agent: Removed
omp(Oh My Pi) from supported agents. Unregistered fromagent.go,help.go,memories.go,update-all.sh,packages.toml,README.md, andAGENTS.md.
- OrbStack Repeated Launch: On macOS, Construct no longer brings OrbStack to the foreground on every invocation when Docker is already running in the background.
startRuntimenow checksdocker infobefore launching OrbStack, avoiding redundantopen -a OrbStackcalls.
- pnpm Update False Failure:
ct sys updatereportedpnpm: FAILEDbecause topgrade's native pnpm step exits non-zero when pnpm is managed by Homebrew. Topgrade's pnpm step is now disabled — brew already handles pnpm updates.
- OpenCode First-Run SQLite Error: Pre-creates
~/.local/share/opencodeand~/.config/opencodedirectories before container startup, preventing theDrizzleError: Failed to run the query 'PRAGMA journal_mode = WAL'failure that occurred on first run in a fresh Construct environment.
- Expanded Default Env Passthroughs: Fresh configs now default
sandbox.env_passthroughto includeGITHUB_TOKEN,GEMINI_API_KEY,OPENAI_API_KEY,ANTHROPIC_API_KEY,QWEN_API_KEY,MINIMAX_API_KEY,KIMI_API_KEY,ZAI_API_KEY,MIMO_API_KEY,OPENCODE_API_KEY, andCONTEXT7_API_KEY.
- Host Service Env: New
host_service_envfield in[sandbox]config section. Injects environment variables into the container withlocalhost/127.0.0.1automatically rewritten tohost.docker.internal. Enables agents inside the sandbox to reach host services like AgentMemory without complex IP detection. Example:"AGENTMEMORY_URL=http://localhost:3111". - AgentMemory config directory (
~/.agentmemory) is now created on container first run.
- Replaced the
[bridge]configuration section andinternal/bridgepackage (IP detection, gateway probing,CONSTRUCT_*env vars) with the simplerhost_service_envmechanism. The old[bridge]config is no longer recognized and should be removed fromconfig.toml.
- Deleted
internal/bridge/package (config, detector, injector, integration). - Removed
[bridge]section from config template andBridgeConfigtype.
- Daemon Restart Command: New
construct sys daemon restartcommand that stops and starts the daemon container in one operation. Handles all container states: missing (starts), stopped (cleans up and starts), and running (stops then starts).
- Host Service Env: Replaced the
[bridge]configuration section withhost_service_envin[sandbox]. Configure environment variables that are injected into the container withlocalhost/127.0.0.1automatically rewritten tohost.docker.internal, enabling agents to reach host services (e.g., AgentMemory) without complex IP detection.
- Hide Secrets Allowlist: Added
hide_secrets_allow_pathsconfiguration option to exclude specific files from redaction. This allows tools that must read files directly (like AWS CLI with~/.aws/credentials) to bypass secret redaction while keeping the security model intact. - Content-Based Scanning: Enhanced secret detection with ripgrep integration for fast pattern-based scanning across all project files.
- Stream-Time Output Masking: Added regex-based secret pattern matching in stdout/stderr to catch API keys, tokens, and credentials in agent output.
- Help Text Simplified: Removed verbose Network Management and Agent Examples sections from main help output for better scannability. Users can access detailed help via namespace-specific commands (
construct network --help). - Help Text Alignment: Fixed column alignment in help text - all descriptions and continuation lines now align properly for improved readability.
- CLI Command Syntax: Corrected documentation to use
construct <agent>instead of non-existentconstruct run <agent>syntax across all documentation files.
- Hide Secrets Experiment Notice: Suppressed "hide_secrets=off" message for users who haven't enabled the experimental feature, preventing unnecessary noise.
- Code Lint Issues: Fixed ineffassign lint error in scanner.go by removing unused args assignment.
- Comprehensive Documentation Restructure: Created 8 new user-facing documentation files organized by topic:
docs/HIDE-SECRETS.md- Complete secret redaction user guidedocs/INSTALLATION.md- Platform-specific installation instructionsdocs/CONFIGURATION.md- Complete configuration referencedocs/SECURITY.md- Security features and best practicesdocs/PROVIDERS.md- Custom Claude API endpoint configurationdocs/PACKAGES.md- Package management guidedocs/AGENTS.md- Complete agent referencedocs/INDEX.md- Documentation navigation hub
- README Streamlining: Reduced README from ~500 lines to ~100 lines with links to detailed documentation.
- Codex Image Paste Reliability (Non-Daemon): Added a dedicated Codex Python PTY wrapper (
construct-codex-wrapper-v1) that intercepts paste keystrokes, fetches PNG data from the host clipboard bridge, saves it in.construct-clipboard/, and injects the file path directly into Codex input. This removes dependence on fragile terminal/X11 clipboard internals. - Copilot PTY Recursion / PTY Exhaustion: Fixed wrapper installation logic to avoid overwriting or self-targeting the real npm-global
copilotbinary, preventing recursive wrapper launches andOSError: out of pty devices. - Copilot Wrapper Install Robustness: Hardened wrapper path selection to install at the active PATH-resolved binary while preserving the true npm-global target for execution.
- Clipboard Debug Coverage: Expanded
construct sys clipboard-debugwith Codex wrapper diagnostics (which codex, wrapper marker/version,_REALtarget, wrapper log tail) to make Codex paste failures directly actionable.
- CI Lint Toolchain Pin: Updated CI
golangci-lintpin fromv2.10.1tov2.11.4in build and release workflows; aligned local Makefile CI pin message to2.11.4. - Codex Clipboard Architecture: Retired the legacy Codex WSL/powershell clipboard fallback path in favor of PTY-wrapper-first handling.
- Entrypoint Clipboard Behavior: Clarified runtime behavior so Codex is excluded from optional X11 sync startup because Codex paste now routes through PTY interception.
- Legacy Codex WSL Clipboard Fallback: Removed WSL env injection (
WSL_DISTRO_NAME,WSL_INTEROP, forcedDISPLAY) from run/exec/daemon env assembly for Codex. powershell.exeShim Path: Removed the fakepowershell.exetemplate and all associated embedding/init/migration/runtime references.- Legacy Codex WSL Docs/Diagnostics: Removed stale WSL/powershell references from clipboard diagnostics and architecture documentation.
- Clipboard & Architecture Docs Refresh: Updated
docs/CLIPBOARD.mdanddocs/ARCHITECTURE-DESIGN.mdto reflect the new PTY-wrapper model for Codex, Copilot wrapperv9, and removal of the WSL/powershell fallback path.
- Claude Code Update False Failure:
claude updateexits non-zero when already up-to-date, causing topgrade to reportClaude Code: FAILEDin the summary even though no update was needed. Fixed by appending|| trueto the command in both the embeddedtopgrade.tomltemplate and the dynamically generated topgrade config (GenerateTopgradeConfig).
- Crush CLI Agent Support: Added Charmbracelet Crush (
crush) as a first-class agent, installed via npm (@charmland/crush), with agent mount registration, AGENTS.md global rules path wiring, help/docs updates, and post-install/post-update verification checks.
- Yolo Agent Coverage: Added
crushto yolo flag handling (--yolo) and updated supported-agent documentation comments. - Agent Install Detection: Included
crushin initial agent-installed detection checks used after image build/setup. - Alias UX Messaging: Updated shell alias onboarding copy to include
crushamong example sandboxed commands.
- Yolo Supported List Accuracy: Corrected README yolo-supported slug list to match runtime behavior.
- Agent Additions Log Completeness: Updated AGENTS.md entry for Crush to include all touched integration files.
- Copilot Image Paste via PTY Wrapper: Replaced the non-functional JS clipboard bridge approach with a Python PTY wrapper that intercepts paste keystrokes at the outer Docker PTY layer, fetches the image from the host clipboard bridge, saves it to
.construct-clipboard/, and injects@pathas typed text into Copilot's input. The JS bridge never fired in headless environments because Copilot's internal clipboard module had no display to read from. - Kitty Keyboard Protocol (KKP) Support: Modern terminals (Ghostty and others using KKP) send Ctrl+V and Cmd+V as CSI-u escape sequences (
\x1b[118;5uand\x1b[118;9u) rather than the legacy\x16control byte. The wrapper now intercepts all three variants via_handle_paste(). - PTY Wrapper PATH Shadowing: The wrapper is installed at
/home/linuxbrew/.linuxbrew/bin/copilot(the Homebrew bin, which takes PATH priority) rather than~/.local/bin/copilotwhich was silently bypassed. The real copilot binary path (~/.npm-global/bin/copilot) is resolved via npm-global candidates and injected at install time so Node's relative module imports resolve correctly. - Clipboard Diagnostic Improvements:
sys clipboard-debugnow shows the resolvedwhich copilotpath, the wrapper version, the_REALbinary path, and the full wrapper log tail for Copilot sessions.
- SSH Agent Access in Daemon Sessions (Linux): Fixed a regression where agents running via
execinto a warm daemon container on Linux could not see the forwarded SSH agent. InjectedSSH_AUTH_SOCK=/ssh-agentinto the environment for all daemon and attachmentexecflows. - Claude & Copilot Image Pasting (Headless/Linux): Enabled reliable image pasting for Claude Code and GitHub Copilot in headless environments by forcing
XDG_SESSION_TYPE=wayland, routing clipboard requests through theclipperbridge. - Multimodal Paste Routing: Added
claudeandcopilotto the file-based paste agent list so they correctly receive@pathreferences for pasted images instead of raw binary data.
- Regression Coverage for Headless Clipboard: Added unit tests ensuring
claude,pi, andcopilotreceive required environment variables for headless clipboard routing. - Regression Coverage for Daemon SSH Agent: Added verification for
SSH_AUTH_SOCKinjection in daemon execution environments on Linux.
- Generic Environment Variable Passthrough: Added first-class
[sandbox].env_passthroughsupport so users can forward arbitrary host environment variables into Construct without editing compose overrides. - Prefix-Based Env Auto-Pass: Added
[sandbox].env_passthrough_prefixeswith default["CNSTR_"], allowing host vars likeCNSTR_CONTEXT7_API_KEYto appear inside Construct asCONTEXT7_API_KEY. - Default Auth Passthroughs: Fresh configs now include
GITHUB_TOKENandCONTEXT7_API_KEYinsandbox.env_passthroughby default. - Regression Coverage for Env Forwarding: Added tests covering explicit env passthrough,
CNSTR_prefix stripping, precedence rules, fresh-run flag injection, daemon exec env assembly, and template/default-config drift.
- Env Precedence Rules: Explicit
sandbox.env_passthroughkeys now win over prefix-derivedCNSTR_passthrough when both target the same inside variable name. - Documentation: Updated README and architecture docs to document generic env passthrough, default forwarded keys, and the new
CNSTR_auto-pass behavior.
- NPM Global Package Updates:
sys updatenow upgrades npm global packages to their latest versions. Previously, topgrade'snpm update -grespected caret semver ranges and would not cross minor/major version boundaries (e.g.,0.58.4would never reach0.60.0). Disabled topgrade's npm step and replaced it with explicitnpm install -g <pkg>@latestfor each globally installed package.
- Oh My Pi Agent: Added Oh My Pi (
omp) as a supported agent — a fork of Pi Coding Agent with Python/IPython integration, LSP support, and extended tooling. Installed via Bun (@oh-my-pi/pi-coding-agent).
- Agent Addition/Removal Docs: Updated AGENTS.md with complete checklist for adding or removing agents, covering all files that need changes.
- Linux Brew Self-Heal: Removed
heal_linux_brew_conflicts()fromupdate-all.shand its associated tests. Thesummarizeformula conflict has been resolved at the image level and this code path was dead.
- Pi Image Paste Routing: Pi agent sessions now force the clipboard path through the
clippershim instead of native X11 bindings, avoiding Linux image-paste failures in the container. - Copilot Clipboard Bridge Reliability: Added a direct patch for
@teddyzhu/clipboardso Copilot can fetch clipboard images from the host bridge even when native clipboard access is unavailable in the container. - Headless Clipboard Sync Timing: Tightened the X11 clipboard sync loop and improved argument handling/debug logging in the clipboard bridge scripts to reduce races when agents read pasted images.
- Linux Brew Update Self-Healing:
construct sys updatenow maintains a Linux self-heal list for unsupported Homebrew formulas, currently replacingsummarizewith the supported npm package@steipete/summarizebefore brew upgrades run.
- Clipboard Debug Command: Added
construct sys clipboard-debugto show clipboard bridge logs, patch state, temporary files, and running sync processes for Copilot debugging. - Clipboard Debug Documentation: Added
docs/CLIPBOARD.mdwith patching details, debug workflow, and expected log locations for clipboard bridge issues. - Updater Regression Guards: Added template coverage for Linux brew self-heal entries and to ensure unsupported fallback packages like
summarizeare not shipped as defaultpackages.tomlinstalls.
- Debug Environment Propagation:
CONSTRUCT_DEBUG=1is now forwarded consistently into run, exec, and daemon flows so clipboard diagnostics work across agent entry paths. - Default Package Template: Added
lftp,tmux, andbtopto the default Homebrew package template. - CLI Help: Updated help output to expose the new
construct sys clipboard-debugcommand.
- Beta Channel Stable Release Detection: Beta-channel update checks now compare both
VERSIONandVERSION-BETA, so beta users receive newer stable releases such as1.4.2even when the beta marker is still on an older prerelease like1.4.0-beta.11.
- Update Channel Regression Coverage: Added tests covering beta users receiving newer stable releases, preferring newer prereleases when beta is ahead, and fallback behavior when one remote marker fetch fails.
- Bun Package Manager Support in
packages.toml: Added a first-class[bun]section so default and user-defined Bun global packages can be managed alongsideapt,brew,npm, andpip. - Doctor Packages Template Drift Detection:
construct sys doctornow warns whenpackages.tomlis missing default template sections, keys, or default list entries such asbun.packages = ["@tobilu/qmd"]. - Doctor Drift Coverage: Added test coverage for missing package sections, missing template list entries, and user override cases that should not be flagged.
- Bun Availability: Bun is now installed unconditionally during setup and exposed on
PATH, instead of being treated as an opt-in[tools]entry. - Default Package Template: Moved
@tobilu/qmdinto the default[bun]package list inpackages.toml. - Local Lint Version Handling: Local
make lintandmake checkno longer fail on patch-levelgolangci-lintdrift; CI remains pinned to the repo version. - CLI Help and README: Updated documentation and help text to reflect Bun support, default Bun installation, and
packages.tomldrift checks inconstruct sys doctor.
- Clipboard Test Lint Failure: Adjusted PNG test fixture allocation to satisfy
prealloclinting and keepmake checkgreen.
- Embedded Gum Confirm Option Styling: Restored proper selected/unselected option spacing and styling in embedded
gum confirmprompts so interactive confirmations no longer render collapsed labels likeYesNo.
- Confirm Style Regression Coverage: Added UI test coverage to ensure embedded confirm defaults retain required prompt and option style spacing.
- Linux Userns/Rootless Runtime Detection: Added runtime-level user namespace remap detection and propagation (
CONSTRUCT_USERNS_REMAP) across compose startup, setup, daemon, and update flows. - Linux Identity Strategy for Rootless Modes: Updated Linux startup behavior to avoid forcing host
UID:GIDmappings when userns remap/rootless mode is active, while preserving existing non-root-strict behavior where applicable. - Entrypoint User Drop Logic (Linux): Entrypoint now keeps namespace-root execution in remapped-userns mode to prevent bind-mount ownership drift during startup bootstrap.
- Exec User Mapping Guardrails:
exec_as_host_userLinux Docker exec mapping is now skipped automatically when userns remap is detected. - Host Alias Targeting:
construct sys aliasesnow resolves aliases through the managedctshim (with executable fallback) instead of bareconstruct, preventing PATH-driven version drift when Homebrew stable and local beta coexist. - Installer Version Normalization:
scripts/install.shnow normalizes incoming versions (strips optionalvprefix) and preserves prerelease identifiers in installed-version detection. - Linux Permission Recovery Flow (Runtime): Replaced the legacy best-effort auto-fix behavior with a strict, interactive repair flow that detects non-writable config paths, prompts for confirmation, and blocks agent execution until ownership is repaired.
- Linux Permission Recovery Flow (Migration): Migration permission recovery now follows the same explicit yes/no prompt model and surfaces exact manual remediation commands when the user declines or repair fails.
- Ownership Remediation Guidance: Runtime and migration flows now present runtime-aware manual commands (
podman unshare chownfor rootless/userns scenarios plussudo chown) instead of a single static hint. - SSH Confirmation UX Fallback: Interactive confirmations now automatically fall back to a plain
[Y/n]prompt in SSH sessions to avoid unreadable remote TUI selection rendering.
- Docker Compose Variable Warnings on Non-Linux Hosts: Fixed spurious
WARN: "CONSTRUCT_HOST_UID/GID/USERNS_REMAP" variable is not setwarnings from Docker Compose when running outside the daemon on macOS. These variables are Linux-only; the compose file now uses${VAR:-}default-value syntax to silently substitute empty/zero defaults instead of warning. - Recurring Config Home Ownership Drift: Fixed repeated ownership drift on
~/.config/construct-cli/homein Linux rootless/userns-remapped Docker/Podman scenarios that caused recurring permission warnings and repair loops. - Doctor Runtime-Aware Remediation:
construct sys doctor --fixnow applies Linux runtime-aware ownership repair paths, includingpodman unsharefirst for Podman rootless and sudo fallback (with prompt when needed). - Doctor Compose Override Reconciliation:
construct sys doctor --fixnow regeneratesdocker-compose.override.ymlfrom current runtime/template settings and validates stale/unsafe user mappings. - Doctor Guidance Accuracy: Linux doctor permission diagnostics now report userns-remap context and show runtime-appropriate manual remediation commands.
- Podman Userns Ownership Re-Drift: Entrypoint startup now skips recursive bind-mount
chown -Rin remapped-userns mode, preventing fixed ownership from being re-corrupted after startup. - Template Path Type Collisions: Migration/init/runtime now self-heal template targets that accidentally exist as directories (for example
entrypoint-hash.sh/,agent-patch.sh/) by replacing them with proper files. - Home Volume Helper Mount Target Collisions (Linux Docker/Podman): Runtime preparation now also normalizes helper mount targets under
~/.config/construct-cli/home/.config/construct-cli/container(install_user_packages.sh,entrypoint-hash.sh,update-all.sh,agent-patch.sh) so stale directory/file-type collisions are repaired before container setup runs. - Fail-Fast Runtime Prep for Mounted Helpers: Runtime now aborts immediately when mounted-helper template preparation fails, instead of continuing into late OCI mount errors during setup.
- False Rebuild Loop on macOS: Agent startup now auto-clears stale
.rebuild_requiredmarkers when the container image entrypoint hash is already current, while still blocking when a rebuild is truly required. - Release/Tag Consistency: Release workflow now rejects
v-prefixed tags and dispatches tap updates with normalized non-prefixed versions. - Legacy Linux 1.3.x → 1.4.x Upgrade Safety: Prevented continuation after unresolved ownership drift in config mount paths by failing early with actionable commands, avoiding partial startup/migration behavior.
- Setup/Spinner Log Peek UX: Pressing Enter to peek logs now shows a clear message when no output has been flushed yet, instead of an empty snapshot.
- Doctor Daemon Recreate Identity Drift (Linux Rootless/Podman):
construct sys doctor --fixdaemon recreation now propagates runtime userns identity context (CONSTRUCT_USERNS_REMAP) so recreated daemon runs don’t trigger immediate post-fix ownership drift warnings. - Migration Side Effects on Invalid Commands: Startup migration checks now run only for recognized commands/subcommands, preventing typos like
construct sys rebuiltfrom triggering migrations before returning an unknown-command error.
- Regression Coverage for Rootless Ownership Fixes: Added/updated unit coverage for userns-aware runtime decisions, Podman rootless fix paths, and sudo fallback behavior.
- Custom Compose Override Opt-In Flag: Added
[sandbox].allow_custom_compose_override(defaultfalse) for advanced users who intentionally managedocker-compose.override.ymlbehavior. - Regression Tests for Marker/Template Edge Cases: Added tests for stale rebuild-marker auto-clear behavior and directory-collision recovery on mounted template helper paths.
- Regression Coverage for Home Helper Mount Collisions: Added runtime tests validating repair of file-vs-directory collisions for
home/.config/construct-cli/containerhelper targets. - Regression Coverage for Command Gating and Doctor Runtime Env: Added tests for migration gating on unknown subcommands and doctor compose env runtime identity propagation.
- Dynamic Custom Provider Aliases:
construct sys aliases --installnow discovers all custom[claude.cc.*]sections from the user'sconfig.tomland automatically installs the correspondingcc-*shell aliases alongside the built-in providers (e.g.[claude.cc.lmstudio]→cc-lmstudio).
- Podman Non-Interactive Setup Runs: Added
-Tto non-interactive composerunflows used by rebuild/setup/update/package install paths to avoid Podman TTY/conmon startup failures on Linux. - SELinux Home-Directory Rebuilds: When SELinux labels are active and Construct is launched from the home directory, compose override generation now falls back container
working_dirto/projectsto prevent Linux startup failures while preserving the project mount. - Broken Gum Binary Fallback: UI now validates that
gumis executable (not only present inPATH) before using it, ensuring clean fallback output and readable setup error logs on Linux.
- Regression Coverage for SELinux Home Fallback: Added Linux runtime test coverage validating
/projectsfallbackworking_dirbehavior when home-directory SELinux relabeling is skipped.
- Linux Startup Identity Propagation: Linux compose runs now propagate host
UID:GIDinto startup env for setup, interactive runs, daemon startup, doctor compose actions, and package/update operations. - Entrypoint Ownership Strategy (Linux): Entrypoint now supports host numeric
UID:GIDownership/runtime mapping (when provided) while preservingHOME=/home/constructsemantics.
- Recurring Linux Home Ownership Drift: Prevented repeated ownership drift on
~/.config/construct-cli/homecaused by container startup user/ownership mismatch across Docker and Podman flows. - Config Permissions Doctor Coverage:
construct sys doctornow reports ownership mismatch (not only writability) for Linux config directories. - Codex Startup Permission Loop: Resolved repeated permission-fix prompts triggered by home mount ownership mismatch before
ct codexstartup.
- Comprehensive Linux
doctor --fixRemediation: Added Linux--fixflow that repairs config ownership/permissions, rebuilds stale/missing image for startup fixes, recycles stale session container, and recreates daemon container. - Regression Coverage for Linux Ownership Fixes: Added unit tests for ownership-state detection, linux fix flow, host identity env injection, daemon recreation/session cleanup fix paths, and template guards for host identity propagation.
- Release Channels (
stable/beta): Addedruntime.update_channelto config, beta version marker support (VERSION-BETA), and installer channel selection (CHANNEL=beta) for selective prerelease adoption. - Semver Prerelease Comparison: Added shared semantic version comparison logic with prerelease support for update checks and migration gating.
- Unified Test Summary (
make test): Added a combined end-of-run summary that reports unit pass/fail/skip counts, unit package pass/fail counts, integration totals, and overall status. - Codex Regression Tests: Added targeted tests for Codex env injection and fallback behavior, including
CODEX_HOMEpresence for Codex runs, WSL fallback env injection when clipboard patching is enabled, and guard checks ensuring non-Codex agents do not inherit Codex-only env vars. - Attach Execution Regression Tests: Added focused tests covering attach-session env injection (
HOME,PATH, clipboard vars, Codex vars), shell fallback behavior, and Linux host-UID exec mapping. - Podman Compose Selection Tests: Added unit coverage to verify command selection prefers
podman-composewhen present and falls back topodman composewhen it is not.
- Test Runner Consolidation:
make testandmake test-cinow run through a sharedscripts/test-all.shflow for consistent unit+integration reporting. - Color Controls for Test Summaries: Added status-aware summary coloring (green/yellow/red) with
NO_COLORto disable andFORCE_COLOR=1to force output coloring. - Codex Config Home Resolution: Force Codex runs (standard + daemon) to use
CODEX_HOME=/home/construct/.codexso config is loaded from/home/construct/.codex/config.tomlinstead of project-relative.codexpaths under/projects/.... - Codex Agent Env Injection Refactor: Centralized Codex-specific run/daemon environment injection into dedicated helper functions to reduce drift across execution paths.
- Attach Execution Path Parity: Attach-to-running-container flows now use interactive exec with the same env protections as normal/daemon runs (construct
PATH,HOME=/home/construct, clipboard vars, and agent-specific env injection). - Linux Non-Daemon Host UID Mapping: Non-daemon Linux Docker agent runs now apply host
UID:GIDmapping whenexec_as_host_user=trueand forceHOME=/home/construct. - Podman Compose Invocation Strategy: Compose command resolution now uses
podman-composewhen available and otherwise falls back topodman compose. - Strict Network Naming Consistency: Strict mode now uses a consistent
construct-netnetwork name across network precreate checks and compose override generation. - Ownership Repair Mapping: Runtime and migration ownership repair commands now use numeric
uid:gidmapping for broader Linux compatibility.
- Entrypoint HOME/Permissions Regression (Linux Docker): Resolved a regression where entrypoint privilege drop could run as a raw host uid:gid without a passwd entry, causing
HOME=/and repeated permission errors writing~/.ssh,~/.bashrc, and setup files. - Daemon Session Startup Reliability: Restored reliable startup behavior for daemon-backed runs (
construct <agent>) when host UID does not exist inside the container. - Linux Config Ownership Auto-Recovery: Hardened migration/runtime permission recovery to attempt non-interactive sudo ownership repair first, with clearer remediation when elevation is unavailable.
- Host UID Exec Fallback Messaging: Ensured fallback warnings are visible when host UID mapping cannot be used inside the container.
- Docker Override Host UID Injection: Removed runtime host UID/GID env injection from generated Docker overrides to avoid reintroducing raw-UID startup regressions.
- Codex Attach Permission Path Drift (Linux): Fixed attach sessions that could miss home/config env injection and fall back to project-relative
.codexresolution. - Podman Runtime/Compose Mismatch: Fixed runtime detection success followed by compose invocation failure on hosts without
podman-compose. - Linux Exec Documentation Drift: Updated docs/comments to match current behavior when host UID is missing in container
/etc/passwd(keep host mapping and forceHOME=/home/construct).
- Compose Network Health Check:
construct sys doctornow detects stale Docker Compose networks that require recreation (for example after Docker daemon/network default changes such as IPv4/IPv6 toggles).
- One-Step Compose Network Recovery:
construct sys doctor --fixnow automatically applies targeted compose network recovery by bringing down the compose stack with orphan cleanup and removing the stale compose network(s) when needed. - Doctor Fix Regression Safety: Added unit coverage for compose-network fix flow (success, no-op, and failure cases) to prevent regressions.
- Issue Template Diagnostics: Added GitHub bug report template fields requiring
construct sys doctoroutput and setup/update logs. - Doctor Environment Visibility:
construct sys doctornow reports host UID/GID, container user mapping, daemon mode details, daemon mount paths, and latest update log path. - Setup/Update Diagnostics: Added richer setup and update diagnostics (UID/GID, PATH, brew/npm/topgrade presence, Homebrew writability checks) plus post-run agent command verification summaries.
exec_as_host_userMode: Added[sandbox].exec_as_host_user(defaulttrue) to run Linux Docker exec sessions as host UID:GID for better host file ownership.
- Docker Linux User Mapping Strategy: Docker overrides no longer force
user:by default on Linux; root-bootstrap remains default and user mapping is now podman-only unless strict mode is explicitly enabled. - Strict Non-Root Mode Documentation: Added explicit warnings and limitations for
[sandbox].non_root_strictin config template, doctor output, and README. - NPM Global Prefix Flow: Setup/update now configure npm global prefix earlier to reduce EACCES failures.
- Agent Installation Continuation: Hardened package install script generation so Homebrew failures do not abort later NPM agent installs.
- Manual
user:Override Recovery (Docker/Linux): Detects unsafe manualuser:mappings indocker-compose.override.yml, warns users, and regenerates override safely (except whennon_root_strict=true). - Entrypoint Shell Setup Noise: Ensured
.bashrcis created before alias-source checks to avoid missing-file warnings. - Daemon Exec UX: Added targeted hinting when daemon exec exits with command-not-found.
- Exec User Fallback Safety: When
exec_as_host_user=true, Construct now verifies host UID exists in container/etc/passwd; if not, it warns and falls back to the container default user. - Stable
ns-*Alias Paths:sys aliases --installnow preserves stable shim paths (for example/opt/homebrew/bin/...) instead of resolving to versionedCellar/Caskroompaths.
- Design Docs Mount Model: Updated
ARCHITECTURE-DESIGN.mdto document the active dual mount behavior:/projects/<folder>for ephemeral runs and/workspaces/<hash>/...for daemon multi-root runs.
- Clipboard Host Command Timeouts (macOS): Added a timeout for
osascriptimage reads in the host clipboard bridge to prevent hangs when reading image clipboard data. - Clipboard HTTP Server Timeouts: Added read/write/idle timeouts to the clipboard HTTP server to avoid stuck connections.
- Codex WSL Fallback Request Timeout: Added
curlconnection and overall time limits inpowershell.exeto avoid indefinite waits when fetching clipboard images. - Codex Daemon Workspace Path Mapping: Added
/mnt/c/workspaces -> /workspacesaliasing and expanded shim path handling so WSL-style fallback paths resolve correctly in daemon sessions.
- Config Defaults Check:
construct sys doctornow reports missing default config keys and can append them with--fix(with backup). - Construct PATH Export: Entry point now writes a construct-managed PATH profile to stabilize login shells.
- Non-Daemon PATH Injection: Inject full Construct PATH for setup, container runs, and daemon exec sessions via
CONSTRUCT_PATH. - PATH Sync Test: Added a unit test to verify PATH parity across Go and template files.
- Clipboard Patch Flag: Added
agents.clipboard_image_patchto enable/disable clipboard image patching and codex WSL clipboard workaround. - Brew Package: Added
nanoto default Homebrew packages.
- Safe Defaults Overlay: Defaults are now applied in code when config values are missing.
- No Config Auto-Merge: Removed automatic
config.tomlmerges and config template hash tracking.
- Agent PATH Parity: Hardcoded PATH across env, entrypoint, compose, and Dockerfile to keep agent binaries available everywhere.
- Setup + Daemon PATH Injection: Ensure full Construct PATH is injected for setup runs and daemon exec sessions so tools are available everywhere.
- Provider Key Passthrough: Always forward common provider API keys into containers, preferring
CNSTR_values and falling back to unprefixed names when empty or missing. Keys:ANTHROPIC_API_KEY,OPENAI_API_KEY,GEMINI_API_KEY,OPENROUTER_API_KEY,ZAI_API_KEY,OPENCODE_API_KEY,HF_TOKEN,KIMI_API_KEY,MINIMAX_API_KEY,MINIMAX_CN_API_KEY.
- Daemon SSH Agent Bridge: Ensure daemon execs initialize the SSH agent proxy and expose
SSH_AUTH_SOCKon macOS so agents can access forwarded keys.
- Daemon Yolo Flag: Avoid injecting
--dangerously-skip-permissionswhen execing into a root-running daemon to prevent permission errors. - Daemon User Enforcement (macOS): Run all agent execs inside the daemon as the
constructuser to avoid root exec on macOS.
- Run User (macOS): Force non-daemon agent runs to use the
constructuser to avoid root exec on macOS.
- Daemon Control Commands: Added
construct sys daemonsubcommands to start, stop, attach, and check status of the background daemon. - Multi-Root Daemon Mounts: Support for multiple host root mounts for the daemon with validation, overlap warnings, and deterministic mount hashing.
- Workspace Mount Path: Removed the legacy
/workspacefallback; project mounts now always use/projects/<folder>viaCONSTRUCT_PROJECT_PATH. - Compose Env Injection:
CONSTRUCT_PROJECT_PATHis now injected automatically for all compose-based commands to keep mount paths consistent.
- Daemon Workdir Mapping: Improved daemon working directory mapping to resolve host paths against validated daemon mounts.
- Shared Entrypoint Hash Helper: Introduced a shared script for computing and writing entrypoint hashes, used by both setup and update flows.
- Agent Patch Script: Centralized clipboard and agent patching into a reusable script.
- Update Flow Patching:
sys updatenow runs agent patching and writes the entrypoint hash immediately to avoid redundant setup on next run. - Mounted Update Scripts: Update and patch scripts are mounted from the host into the container for faster iteration without rebuilds.
- Update Fallback Warning: Warns when falling back to
/usr/local/bin/update-all.shand suggests runningconstruct sys refresh.
- Post-Update Double Setup: Avoids re-running full setup after updates when only patching is needed.
- Rebuild Template Duplication:
construct sys rebuildno longer copies container templates twice during automatic migration + refresh. - Daemon Mount Working Dir: Agent exec in the daemon now maps the host working directory to the correct mounted path.
- Multiple performance optimizations where implemented to make the CLI faster and more efficient. Now it's fast. Really fast.
- Daemon Auto-Start Service: Added
sys daemon install,sys daemon uninstall, andsys daemon statusfor managing a background daemon service (systemd) that can auto-start on login/boot. - Daemon Auto-Start Config: New
[daemon] auto_startsetting to start the daemon on first agent run for faster subsequent startups.
- Faster Runtime Detection: Runtime detection now checks container, podman, and docker in parallel to reduce startup latency on multi-runtime systems.
- Daemon Exec Fast Path: When a daemon container is running, agents run via
execinstead ofcompose runfor much faster startup. - Compose Override Caching: Docker compose override generation is cached to avoid unnecessary regeneration on repeated runs.
- Parallel Runtime Detection: Checks container, podman, and docker concurrently to cut detection latency by 0.5-1s.
- Daemon Exec Path: Agent runs reuse the warm daemon container for 2.5-7s faster startup.
- Entrypoint Caching: Skips expensive clipboard and agent patching when entrypoint hash is unchanged (200-800ms saved).
- Preemptive Services: Clipboard server and SSH bridge already start asynchronously; no added latency from sequential startup.
- Daemon Staleness Guard: Detect stale daemon containers (old image) and fall back to normal startup with clear guidance to restart the daemon.
- Daemon Shell Exec:
construct sys shellnow execs a default shell when attaching to a running daemon, preventing emptyexeccalls. - Post-Update Entrypoint Patching: Clearing the entrypoint hash after updates ensures new agents get patched correctly.
- OrbStack/Podman Sudo Compatibility: Fixed package install/update flows (
sys packages --installandsys update) failing with "PAM account management error" in environments where sudo is unavailable or misconfigured (OrbStack, rootless Podman, minimal containers). Thanks @KingMob for the bug report.- Install scripts now detect if running as root (no sudo needed) or test sudo availability before use
- Gracefully skips privileged apt operations when sudo unavailable instead of failing
- Applies to both
install_user_packages.sh(generated) andupdate-all.sh(template)
- Production Ready: Marked Construct CLI as production ready.
- SELinux Label Control: Added
sandbox.selinux_labelsconfig to enable, disable, or auto-detect SELinux mount labels. - Doctor Ownership Check: Added a Linux/WSL config permissions check with a
chownfix suggestion for~/.config/construct-cli. - Automatic Config Permission Fix: On Linux/WSL, automatically detect and fix config directory ownership issues before runtime preparation, with clear messaging and user confirmation before running sudo.
- Simple Progress Mode: Added dot-based progress output for non-TTY environments and when
CONSTRUCT_SIMPLE_PROGRESS=1is set. - Podman Rootless Support: On Linux, container now runs as user (not root) for proper Podman rootless compatibility. macOS continues to use root with gosu drop.
- Truecolor Support: Forward host
COLORTERMto container, defaulting totruecolorwhen unset. Fixes washed-out colors over SSH.
- SELinux Home Relabeling: Skip
:zlabels when running from the home directory to avoid relabel errors. - Config Write Guidance: Emit clearer permission warnings when config-generated files cannot be written.
- Goose CLI: Added
gooseagent, with first-run configure guidance. - Droid CLI: Added
droidagent. - Kilo Code CLI: Added
kilocodeagent. - Yolo Mode Config: Added
[agents]config to enable yolo flags per-agent or globally inconfig.toml. - Agent Browser: Added
agent-browserto default npm packages and configured its post-install dependency setup. Headless browser automation CLI for AI agents. Fast Rust CLI with Node.js fallback. No MCP required. - LiteLLM: Added
litellmto default pip packages; open-source LLM gateway/SDK with a unified OpenAI-compatible API across 100+ providers, with cost/error handling and failover. - Post-Install Hooks: Added
[post_install].commandstopackages.toml, executed after all package managers finish.
- Podman Compose: Added
podman-composeto default brew packages inpackages.toml.
- Non-Sandboxed Aliases:
ns-entries are now shell functions in the RC file, forwarding flags and args without installing extra files. - Agent Rules Bulk Replace:
sys agents-mdnow supports replacing all agent rules at once with a single pasted prompt, including Copilot frontmatter.
- Stale Entrypoint Detection: Detect and prompt rebuild when the container image entrypoint is out of date to avoid repeated setup spinners.
- Pi Coding Agent: Added support for Pi Coding Agent (
@mariozechner/pi-coding-agent).- Config mounted at
/home/construct/.pi - Added to default npm packages in
packages.toml - Auto-creates
~/.pi/agent/auth.json(empty object) on first run
- Config mounted at
- Container User Authentication: Set fixed password "construct" for the construct user to enable sudo access when running commands inside
sys shell.- All automated operations (init, build, migrate, update, rebuild) remain completely passwordless via NOPASSWD sudoers configuration
- Static Site Generators: Added Hugo and Zola to default brew packages in
packages.toml. - Ruby Gems Support: Added support for installing Ruby gems via a new
[gems]section inpackages.toml.- Includes
jekyllas default gem - Integrated with package installation scripts and Topgrade updates
- Includes
- Host System Info: Added new "Host System" check to
sys doctorto display OS, architecture, and version details.- Shows macOS version and kernel on Apple/Intel Macs
- Shows Linux distribution name and kernel version
- Detects and displays WSL environment on Windows
- Doctor SSH Keys Display: Fixed SSH key listing in
sys doctorto exclude non-key files (known_hosts,config,agent.sock, etc.).- Renamed check from "Local SSH Keys" to "Construct SSH Keys" for clarity
- Keys shown are those stored in Construct's SSH directory (whether imported or generated)
- Agent PATH Visibility: Fixed issue where agents (particularly
codex) couldn't see binaries in PATH when running commands via their Bash tools.- Root cause:
/etc/profilewas resetting PATH when bash spawned, wiping out Homebrew paths - Patched
/etc/profilein entrypoint.sh (idempotent, no image rebuild needed) - Centralized PATH definition in
internal/env/env.go(DRY principle) - Synchronized PATH configuration across docker-compose.yml, entrypoint.sh, and env.go
- Ensures all agent subprocesses inherit full PATH including Linuxbrew, Cargo, npm-global, etc.
- Root cause:
- CT Symlink Stability:
ctnow targets the stable Homebrew path on macOS/Linux, andsys doctorself-heals broken Cellar-based symlinks. - Agent Detection in Doctor: Agent install check now verifies binaries inside the container so Homebrew/NPM-based installs are detected correctly.
- Rebuild Help Clarity:
sys rebuildhelp text now explicitly mentions it runs migrate before rebuilding.
- md-over-here: Added
md-over-hereto default brew packages inpackages.tomltemplate. - Brew Installation Detection:
self-updateand update notifications now detect if the CLI was installed via Homebrew and provide appropriate instructions (brew upgrade estebanforge/tap/construct-cli) instead of attempting a manual binary overwrite.
- Package Installation Reliability: Improved package install flow (
sys packages --install) to userun --rminstead ofexec, allowing it to work correctly even if The Construct is not already running. - Initialization Consistency: Updated
sys initandsys rebuildto always perform a full migration (syncing config and templates) before building the image. - Migration Messaging: Clarified migration messages to indicate when an image is "marked for rebuild" versus being actively rebuilt.
- Container Rebuild Reliability: improved
sys rebuildto force fresh container images.- Now stops and removes running containers and images before rebuilding (not just marking for rebuild)
- Added support for macOS 26+ native
containerruntime management.
- Container Image Optimization: Updated base image to a more stable version.
- Leaner base image means faster rebuilds and reduced storage footprint.
- Template Synchronization: Improved migration flow (
sys config --migrate) to ensure binary rebuilds are correctly triggered when embedded templates change.- Automatic removal of old Docker image (forces rebuild with new Dockerfile)
- New hash-based template change detection (more reliable than version checks)
- Version-Independent Aliases: Fixed Homebrew alias installation to use version-independent paths.
- Aliases now use
constructcommand instead of hardcoded Cellar paths (e.g.,/opt/homebrew/Cellar/construct-cli/0.11.0/bin/construct) - Aliases remain functional after Homebrew updates without reinstallation
- Also improves portability for curl-based installations and local builds
- Aliases now use
- Chromium Multi-Arch Support: Fixed Puppeteer-based tools (url-to-markdown, browser automation) on arm64 hosts.
- Installed system Chromium from Debian repos (automatically matches container architecture)
- Configured Puppeteer to use system Chromium instead of downloading x86-64 version
- Prevents "Dynamic loader not found: /lib64/ld-linux-x86-64.so.2" errors on Apple Silicon/arm64
- Includes all required Chromium dependencies (fonts, GTK, NSS, etc.)
- Homebrew Installation: Added support for installing via Homebrew (Linux & macOS) using
brew install EstebanForge/tap/construct-cli. - Topgrade Integration: Automated package updates inside Construct via Topgrade
- Ensures all system packages, language tools, and development utilities stay current
- Seamless integration with Construct's containerized environment
- Configurable via
packages.tomlfor custom update policies
- Worktrunk.dev Support: Default installation and usage of Worktrunk for simultaneous agent collaboration
- Enables multiple AI agents to work on the same codebase without conflicts
- Provides intelligent workspace isolation and synchronization
- Configured by default for optimal multi-agent workflows
- Cargo Package Support: users can now install Rust-based tools and utilities via a new
[cargo]section inpackages.toml. - Centralized Debugging: unified all debug logging under the
CONSTRUCT_DEBUGenvironment variable.- When enabled (
CONSTRUCT_DEBUG=1), logs are written to~/.config/construct-cli/logs/on the host. - Container-side logs (like
powershell.exeandclipboard-x11-sync) are redirected to/tmp/for guaranteed visibility and write access. - Replaced legacy
CONSTRUCT_CLIPBOARD_LOGwith the new unified system.
- When enabled (
- Non-Sandboxed Agent Aliases:
sys aliases --installnow createsns-*prefixed aliases for agents found in PATH.- Example:
ns-claude,ns-gemini, etc. run agents directly without Construct sandbox - Useful for running agents with full host access when needed
- Example:
- Alias Re-installation:
sys aliases --updatenow supports updating existing installations.- Detects existing aliases and offers to re-install with gum confirmation prompt
- Automatically removes old alias block before installing fresh
- Adds missing
ns-*aliases when updating existing installations
- Shell Config Backups: automatic timestamped backups before modifying shell config files.
- Creates
.backup-YYYYMMDD-HHMMSSfiles before any changes - Applies to both
.zshrc,.bashrc, and.bash_profile - Protects users from critical shell configuration errors
- Creates
- Sandbox Isolation: sanitized the
PATHenvironment variable across all templates to prevent host-side directory leakage into the sandbox. - Streamlined Installation:
- Eliminated redundant package installations by removing duplicates between APT and Homebrew phases.
- Added
DEBIAN_FRONTEND=noninteractiveto silence UI dialogs during container setup.
- Improved Migration Experience: refined version detection messaging to accurately distinguish between binary upgrades, downgrades, and template-only syncs.
- Codex Image Paste: restored full image support for OpenAI Codex CLI agent after its internal mechanism changed from direct binary data to path-based references.
- Fixed
Ctrl+Vshortcut by shimming the WSL clipboard fallback with a smartpowershell.exeemulator that returns workspace-compatible paths. - Implemented
/mnt/c/projectsand/mnt/c/tmpsymlinks within the container to ensure Codex path resolution works seamlessly across all host platforms. - Improved "New Flow" paste detection by allowing Codex to receive raw image paths instead of multimodal
@pathreferences.
- Fixed
- Self-Update Reliability: fixed a confusing state where
self-updatewould report an upgrade from0.3.0due to intentional version file deletion. - Clipboard Image Paste: Centralized file-based paste agent list (
gemini,qwen,codex) into a single constant to prevent drift. - Container Rebuild Reliability: improved
sys rebuildto force fresh container images.- Now stops and removes running containers and images before rebuilding (not just marking for rebuild)
- Added
--no-cacheflag to build command to bypass Docker layer cache - Added support for Apple container runtime (macOS 26+) alongside Docker and Podman
- SSH Key Prioritization: Enhanced SSH configuration management to ensure correct key selection order for all hosts.
- Auto-generates
~/.ssh/configwith SSH agent support and key prioritization (defaultandpersonalkeys tried first). - Applies to all SSH connections (GitHub, GitLab, private servers, etc.), not just specific hosts.
- Adds RSA key algorithm support (
PubkeyAcceptedAlgorithms +ssh-rsa) for legacy servers. - Falls back to physical SSH keys if present after trying agent keys.
- Automatic updates on CLI upgrade unless user opts out with
construct-managed: falseflag. - Creates backup (
~/.ssh/config.backup) before each update.
- Auto-generates
- User-Defined Package Management: Customize your sandbox environment with
packages.tomlconfiguration.- Install additional APT, Homebrew, NPM, and PIP packages beyond the defaults.
- Easy activation of specialized version managers: NVM, PHPBrew, Nix, Asdf, Mise, VMR, Volta, and Bun.
- New
construct sys packagescommand to quickly edit package configuration. - New package install command
construct sys packages --installto apply package changes to running containers without restart. - Package configuration persists across updates and environments.
- Dynamic Project Mount Paths: Agents now mount the current host directory to
/projects/<folder_name>instead of static/workspace.- Improves agent contextual awareness and long-term memory.
- Dynamically calculates mount path based on current directory name.
- Automatically updates all internal templates (Dockerfile, docker-compose) and helper scripts.
- Preserves compatibility with non-standard folder names (spaces, special characters).
- Faster Docker Builds: Streamlined base image for significantly faster build times.
- Reduced Dockerfile to only essential build-time dependencies.
- Moved most packages to runtime installation during first container start.
- Critical packages verified at startup to ensure reliability.
- Leaner base image means faster rebuilds and reduced storage footprint.
- Enhanced Code Quality: Internal refactoring and expanded test coverage.
- Comprehensive clipboard functionality test suite.
- Updated linter configuration for stricter code quality enforcement.
- Improved error handling and package naming conventions.
- Package Management Refinements:
- PHP extensions (PCOV) now installed via Homebrew tap instead of hardcoded scripts for better maintainability.
- Node.js version unlocked from
node@24tonodefor automatic latest stable version. - Native Node.js module compilation support with automatic compiler symlinks (
g++-11→g++-14). - Removed conflicting
bash-completionpackage to prevent installation failures.
- Clipboard Image Pasting: Fixed image pasting across agents with image-first handling, normalization/resize, and
@pathonly for Gemini and Qwen.
- Reliable Agent Installation: Overhauled
entrypoint.shto prevent partial installation failures.- Split large
brew installcommands into categorized blocks (Core, Dev, Languages, Linters, Web/Build). - Unified npm package installation into a single, efficient command.
- Implemented hash-based change detection for
entrypoint.shto automatically trigger re-installation when scripts are updated.
- Split large
- Smart Runtime Configuration:
- Optimized Linux runtime detection to avoid unnecessary user ID mapping for UID 1000 users, enabling proper permission fixups.
- Improved migration flow (
sys config --migrate) to ensure binary rebuilds are correctly triggered when embedded templates change.
- Permission Issues: Fixed permission errors during initial setup by allowing the container to start as root for volume ownership fixes before dropping privileges.
- Missing Tools: Resolved issue where tools like
golangci-lintcould be missing due to silent installation failures. - Build Caching: Fixed an issue where Docker build cache would persist stale
entrypoint.shversions, preventing updates from being applied.
- Git Identity Inheritance: Automatically propagates host git identity (
user.nameanduser.email) to the container environment.- Solves commit attribution issues inside the container.
- Enabled by default, configurable via
propagate_git_identityinconfig.toml. - Safely injects values as
GIT_AUTHOR_NAME,GIT_AUTHOR_EMAIL, etc., without mounting potentially incompatible host git configs.
- Improved Shell Prompt: Container hostname is now set to
sandbox(was random ID) for a cleaner prompt experience:construct@sandbox:/workspace$. - Headless Login Bridge: New
construct sys login-bridgecommand to enable local browser login callbacks for headless-unfriendly agents (Codex, OpenCode with OpenAI GPT or Google Gemini).- Runs until interrupted and forwards
localhostOAuth callbacks into the container.
- Runs until interrupted and forwards
- Global Agent Rules Management: New
construct sys agents-mdcommand to manage rules for all supported agents in one place.- Interactive selection UI powered by
gum. - Supports: Gemini, Qwen, OpenCode, Claude, Codex, Copilot, and Cline.
- Auto-Initialization: Automatically creates missing rules files and parent directories on demand.
- Open All: "Open all Agent Rules" option to quickly edit all supported agent rules at once.
- Context-Aware Expansion: Automatically resolves
~to the Construct persistent home directory (~/.config/construct-cli/home/) so rules are correctly applied within the container environment. - Fallback UI: Seamlessly falls back to a numeric menu if
gumis not available.
- Interactive selection UI powered by
- Generic Workspace Path: Renamed the internal container mount point and working directory from
/appto/workspace.- This reflects the project's evolution into a general-purpose sandbox for any CLI agent, not just those used for "app" development.
- Automatically updates all embedded templates (Dockerfile, docker-compose) and runtime override generation.
- Updated design and user documentation to reflect the new path.
- Improved UI grammar and messaging consistency across system commands.
- Updated
.gitignoreto prevent tracking of local debug logs while preserving them for development.
- Secure SSH Agent Forwarding: Automatic detection and secure mounting of the local SSH agent into the container.
- Supports Linux and macOS (OrbStack and Docker Desktop).
- Implements a TCP-to-Unix bridge for robust macOS/OrbStack connectivity, bypassing common permission and socket quirks.
- Fully configurable via
forward_ssh_agentinconfig.toml(enabled by default).
- SSH Key Import System: New
construct sys ssh-importcommand to securely bring host keys into Construct.- Interactive multi-select UI powered by
gum. - Automatic permission fixing (0600) and matching
.pub/known_hostssupport. - Smart logic to skip selection if only one key is found.
- Interactive multi-select UI powered by
- Config Restoration: New
construct sys config --restorecommand to immediately recover from configuration backups. - Shell Productivity Enhancements:
- Automatic management of
.bash_aliasesinside the container. - Standard aliases included:
ll,la,l, and color-codedls/grep. - Zsh-like navigation shortcuts:
..,...,.....
- Automatic management of
- Improved Diagnostics:
construct sys doctornow reports SSH Agent connectivity and lists imported local keys.
- Non-Destructive Migration: Redesigned migration flow (
sys config --migrate) logic to be strictly additive.- Preserves all user comments and formatting.
- Automatically identifies and preserves custom Claude Code aliases, moving them to a dedicated "User-defined" section.
- Prevents TOML section duplication.
- Unified macOS Magic Path: Simplified container orchestration to use industry-standard magic paths for macOS container bridges.
- Entrypoint Reliability: Container now starts as root to perform critical permission fixes (SSH sockets, home directory ownership) before dropping to the non-privileged
constructuser viagosu.
- Fixed "Permission denied" errors when accessing mounted SSH sockets on macOS.
- Fixed "Communication failed" errors by implementing a Go-native TCP bridge for macOS.
- Full compliance with strict
golangci-lintrules, improving overall code robustness and error reporting. - Simplified
config.tomltemplate to reduce clutter and minimize merge conflicts.
- Self-Update Command:
construct sys self-updatenow downloads and installs the latest version directly from GitHub releases- Automatic platform detection (darwin/linux, amd64/arm64)
- Prompts for confirmation when already on latest version
- Backup and restore on failure
- Smart Config Migration: Config merging now only runs when template structure actually changes
- Hash-based detection of config template changes
- Skips unnecessary backup/merge cycles on patch version updates
- Container templates still update on every version bump (for bug fixes)
- Simplified Update Check: Version checking now uses lightweight
VERSIONfile instead of GitHub API- Faster checks, no API rate limits
- Download URLs constructed directly from version string
- Install Script Improvements:
- Checks for existing installation before downloading
- Prompts user if same version already installed
- Uses
VERSIONfile for remote version lookup - Reads local
.versionfile first, falls back to querying binary - New
FORCE=1env var to skip version check
- Version command no longer triggers config initialization
- Update check now uses proper semver comparison (was treating any version difference as "update available")
make lintnow runs golangci-lint for parity with CI checks.
- Migration merge now skips incompatible types and validates TOML, preventing corrupted config files after upgrades.
- Improved error handling and warnings for clipboard server, daemon UI rendering, log cleanup, and shell alias flows.
- Automatic Migration System: Seamless upgrades with zero user intervention
- Version tracking via
.versionfile in config directory - Automatic detection of version changes on startup
- Smart detection of 0.3.0 → 0.4.0 upgrades (handles missing version file)
- Improved Config Merging: New template-first merge logic that preserves comments and layout while syncing supported user values.
- Automatic replacement of container templates with new versions
- Automatic removal of old Docker image (forces rebuild with new Dockerfile)
- Persistent volumes preserved (agents, packages, configurations)
- Backup of old config created during migration (
config.toml.backup) - Clear migration progress output with success/error reporting
- New migration command
construct sys config --migratefor manual config/template refresh (useful for debugging)
- Version tracking via
- Expanded Container Toolchain: Added comprehensive language support to the sandbox:
- Languages: Rust, Go, Java (OpenJDK), Kotlin, Swift, Zig, Ruby, PHP, Dart, Perl, Erlang, COBOL.
- Build Tools: Ninja, Gradle, UV, Composer.
- Utilities:
jq,fastmod,tailwindcssCLI.
- Cross-Boundary Clipboard Bridge: Unified host-container clipboard for seamless text and image pasting
- Secure Host-Wrapper Bridge: A secure Go HTTP server on the host provides authenticated clipboard access to the container via ephemeral tokens.
- Universal Image Support: Robust support for pasting images directly into agents across macOS, Linux, and Windows (WSL).
- Multi-Agent Interception: Automatic shimming of
xclip,xsel, andwl-pasteinside the container to redirect calls to the bridge. - Dependency Patching:
entrypoint.shautomatically finds and shims nested clipboard binaries innode_modules(fixes Gemini/Qwenclipboardyissues). - Tool Emulation: Fake
osascriptshim allows agents to use macOS-native "save image" logic while running on Linux. - Smart Path Fallback: Automatically saves host images to
.gemini-clipboard/and returns multimodal@pathreferences for agents expecting text. - Runtime Code Patching: Automatically bypasses agent-level
process.platformchecks that would otherwise disable image support on Linux. - Zero Config: Transparently handles all platform-specific clipboard complexities with no user setup required.
- Development Installation Scripts: New tools for local testing and debugging
install-local.sh: Full-featured install with automatic backups and verification (defaults to~/.local/bin)dev-install.sh: Lightning-fast dev install for rapid iteration (no confirmations, no backups)uninstall-local.sh: Safe uninstall with backup restoration options- New Makefile targets:
install-local,install-dev,uninstall-local
- Comprehensive Development Documentation
- New
DEVELOPMENT.mdwith complete development workflow guide - Detailed installation methods, testing procedures, and troubleshooting
- VS Code tasks configuration examples
- New
- Help Text Alignment: All CLI help descriptions now properly aligned for better readability
- Aligned
#comments across all help sections (sys, network, daemon, cc) - Consistent formatting in main help, network help, daemon help, and provider help
- Aligned
- Installation Defaults: Local installation scripts now default to
~/.local/bin(no sudo required)- Users can override with
INSTALL_DIRenvironment variable - Improved user experience for development workflows
- Users can override with
- Clipboard Integration Architecture: Upgraded from manual directory sync to a secure, real-time HTTP bridge.
- Transparent redirection of all terminal clipboard tools to the host system.
- Integrated support for multimodal agents (Claude, Gemini, Qwen).
- Runtime Package Conflicts: Resolved naming collision in
internal/agent/runner.go- Standard library
runtimepackage now imported asruntimepkg - All runtime function calls updated to use proper package reference
- Standard library
- Version Location: Updated version references in documentation to reflect actual location
- Version now correctly documented as
internal/constants/constants.go(notmain.go)
- Version now correctly documented as
- Updated README.md with cross-boundary clipboard bridge instructions
- Added DEVELOPMENT.md with development workflow and testing guide
- Updated AGENTS.md with correct file paths and new clipboard features
- Updated ARCHITECTURE-DESIGN.md with detailed clipboard bridge architecture
- Core CLI Framework: Single-binary CLI for running AI agents in isolated containers
- Runtime auto-detection: macOS
container→podman→docker - Embedded templates (Dockerfile, docker-compose, entrypoint, configs)
- Self-building on first run with
construct sys init
- Runtime auto-detection: macOS
- Network Isolation: Three modes for security
permissive: Full network access (default)strict: Custom network with domain/IP allowlist/blocklistoffline: No network access at all- Live UFW rule application while agents are running
- Agent Support: Pre-configured support for multiple AI agents
- Claude Code, Gemini CLI, Qwen Code, GitHub Copilot CLI
- OpenCode, Cline, OpenAI Codex
- Agent configuration directories mounted from host
- System Commands (
construct sys):init: Initialize environment and install agentsupdate: Update agents to latest versionsreset: Delete volumes and reinstallshell: Interactive shell with all agentsaliases --install: Install agent aliases to host shellversion: Show versionconfig: Open config in editoragents: List supported agentsdoctor: System health checksself-update: Update construct binarycheck-update: Check for available updates
- Network Commands (
construct network):allow <domain|ip>: Add to allowlistblock <domain|ip>: Add to blocklistremove <domain|ip>: Remove rulelist: Show all rulesstatus: Show active UFW statusclear: Clear all rules
- Daemon Mode (
construct sys daemon):start: Start background containerstop: Stop background containerattach: Attach to running daemonstatus: Show daemon status
- Claude Provider Aliases (
construct cc):- Support for alternative Claude-compatible API endpoints
- Providers: Z.AI, MiniMax, Kimi, Qwen, Mimo
- Environment variable expansion and automatic reset
- Configuration in
config.tomlunder[claude.cc.*]
- Persistent Volumes: Agent installs persist across runs
construct-agents: Agent binaries and configsconstruct-packages: Homebrew packages- Ephemeral containers (
--rm) for clean host system
- Auto-Update System:
- Passive background update checks (configurable interval)
- Desktop notifications for available updates
- Self-update command for binary upgrades
- Version checking against GitHub releases
- Platform Support:
- macOS (Intel and Apple Silicon)
- Linux (amd64 and arm64)
- SELinux support with automatic
:zlabels - Linux UID/GID mapping in generated override files
- WSL2 compatibility
- Main config at
~/.config/construct-cli/config.toml - Runtime engine selection (auto, container, podman, docker)
- Network mode configuration with domain/IP lists
- Provider-specific environment variables
- Auto-update check settings
- Makefile with comprehensive build targets
- Cross-compilation support for all platforms
- Unit and integration test suites
- CI-ready lint and test commands
- GitHub Actions integration ready
- Install/uninstall scripts
- Comprehensive README.md with examples
- ARCHITECTURE-DESIGN.md with architecture details
- AGENTS.md for code agents working on the project
- CONTRIBUTING.md for contributors
- LICENSE.md (MIT)