Skip to content

Commit fc614e0

Browse files
committed
Mount repo at its exact host path instead of /workspace
On Linux and macOS the repo is now bind-mounted at its real absolute path (e.g. /home/user/src/myrepo) with the container workdir set to match. This makes git worktrees work correctly — git stores absolute host paths in worktree metadata, which now resolve identically inside the container. It also allows running construct against multiple repos or worktrees simultaneously without path conflicts. CONSTRUCT_WORKSPACE_PATH is injected as an env var and expanded into ~/.config/opencode/AGENTS.md at container start so the agent knows which directory is shared. On Windows Docker Desktop cannot mirror host paths into the Linux VM, so /workspace is kept as a fallback. Requires --rebuild to pick up the updated entrypoint in the tool image. Generated by construct
1 parent b7062db commit fc614e0

8 files changed

Lines changed: 370 additions & 30 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## [Unreleased]
44

5+
### Changed
6+
- **Path mirroring: repo mounted at its exact host path** — the container no longer mounts the repo at the fixed path `/workspace`. On Linux and macOS the repo is now mounted at its real absolute path (e.g. `/home/user/src/myrepo`), and the container's working directory is set to that same path. This makes git worktrees work correctly (git stores absolute host paths in worktree metadata, which now resolve identically inside the container), and allows running `construct` against multiple repos or worktrees simultaneously without path conflicts. The agent is informed of the shared path via the `CONSTRUCT_WORKSPACE_PATH` environment variable, which is expanded into `~/.config/opencode/AGENTS.md` at container start. On Windows, Docker Desktop cannot mirror host paths into the Linux VM, so the previous `/workspace` fallback is retained.
7+
58
---
69

710
## [v0.9.0] — 2026-03-12

docs/spec/construct-agents-md.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,24 @@ You are running inside a construct container.
3535

3636
## Workspace
3737

38-
`/workspace` is the user's repository, bind-mounted from their machine.
38+
`<CONSTRUCT_WORKSPACE_PATH>` is the directory shared with the user,
39+
bind-mounted from their machine at its exact host path.
3940
Changes you make there are immediately visible to the user.
40-
This is the only directory shared with the user.
4141

4242
## Isolation
4343

44-
Everything outside `/workspace` is isolated inside the container.
44+
Everything outside the shared directory is isolated inside the container.
4545
Your home directory (`/home/agent`) persists across sessions via a named Docker
4646
volume, so shell history, tool caches, and config files survive container
4747
restarts. The user's machine is separate — you cannot reach their localhost and
4848
they cannot reach yours.
4949
```
5050

51+
`CONSTRUCT_WORKSPACE_PATH` is injected as an env var by the runner and expanded
52+
by the entrypoint shell when writing the file. On Linux/macOS it holds the real
53+
absolute host path (e.g. `/home/user/src/myrepo`); on Windows it is
54+
`/workspace` (Docker Desktop fallback).
55+
5156
The networking section (see below) is appended after this block.
5257

5358
### When `--port` is used (`CONSTRUCT_PORTS` is set)

docs/spec/global-commands.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
OpenCode loads custom slash commands from two locations:
66

7-
- **Per-project:** `.opencode/commands/` in the repo root (already works — the repo is bind-mounted at `/workspace`)
7+
- **Per-project:** `.opencode/commands/` in the repo root (already works — the repo is bind-mounted at its exact host path)
88
- **Global:** `~/.config/opencode/commands/` in the user's home directory
99

1010
The construct agent container has an isolated named Docker volume for `/home/agent`, so the host's `~/.config/opencode/commands/` directory is never visible to the agent. Global slash commands defined on the host are silently absent.

docs/spec/path-mirror.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Path mirroring: mount the host path at its exact container location
2+
3+
## Problem
4+
5+
The previous approach always mounted the user's repository at the fixed path
6+
`/workspace` inside the container:
7+
8+
```
9+
-v /home/user/src/myrepo:/workspace:z -w /workspace
10+
```
11+
12+
This creates two related problems:
13+
14+
**1. Git worktrees break.** Git worktrees store their metadata using absolute
15+
host paths (`/home/user/src/myrepo/.git/worktrees/feature/gitdir`,
16+
`/home/user/src/myrepo-feat/.git``/home/user/src/myrepo/.git`). When the
17+
container only sees the path as `/workspace`, git cannot resolve these
18+
cross-references and operations like `git status`, `git log`, and branch
19+
switching fail.
20+
21+
**2. Multi-repo workflows require re-mounting.** With the serve/client
22+
architecture, users want to open multiple workspaces (separate repos or
23+
worktrees) without stopping the host client. The fixed `/workspace` path makes
24+
it impossible to work in two containers simultaneously where both paths need
25+
to coexist on the filesystem.
26+
27+
## Solution
28+
29+
On Linux and macOS, mount the host path at its exact absolute location inside
30+
the container:
31+
32+
```
33+
-v /home/user/src/myrepo:/home/user/src/myrepo:z -w /home/user/src/myrepo
34+
```
35+
36+
The container's working directory is set to the same path. Git's internal path
37+
references now resolve correctly because the container filesystem mirrors the
38+
host layout for the mounted subtree.
39+
40+
A `CONSTRUCT_WORKSPACE_PATH` environment variable is injected into the
41+
container, carrying the container-side path. The entrypoint uses it when
42+
writing `~/.config/opencode/AGENTS.md` to tell the agent which directory is
43+
shared with the user.
44+
45+
No `/workspace` symlink or alias is created. The fixed path `/workspace` is
46+
gone from the container filesystem entirely (on Linux/macOS).
47+
48+
## Windows
49+
50+
On Windows, `os.Getuid()` returns `-1` (Docker Desktop runs containers through
51+
a Linux VM where host UID/GID have no meaning). Windows paths (`C:\Users\...`)
52+
have no valid equivalent Linux path, so path mirroring is not applied.
53+
Windows falls back to the previous behaviour: the repo is mounted at
54+
`/workspace` and `CONSTRUCT_WORKSPACE_PATH=/workspace`.
55+
56+
## Behaviour
57+
58+
| Platform | Host path | Container mount point | `-w` workdir |
59+
|---|---|---|---|
60+
| Linux/macOS | `/home/user/src/myrepo` | `/home/user/src/myrepo` | `/home/user/src/myrepo` |
61+
| Linux/macOS | `/home/user/src/myrepo-feat` | `/home/user/src/myrepo-feat` | `/home/user/src/myrepo-feat` |
62+
| Windows | `C:\Users\user\src\myrepo` | `/workspace` | `/workspace` |
63+
64+
### Git worktree example
65+
66+
```
67+
# Host layout
68+
/home/user/src/
69+
myrepo/ ← main worktree (construct /home/user/src/myrepo)
70+
myrepo-feat/ ← linked worktree (construct /home/user/src/myrepo-feat)
71+
```
72+
73+
Both containers now see the paths that git's worktree metadata references, so
74+
all git operations work correctly. Each container gets its own home volume
75+
(home volume name is keyed by SHA256 of the repo path, so they are always
76+
distinct).
77+
78+
### Opening multiple workspaces
79+
80+
Because each container's working directory is the real host path, users can
81+
run `construct` against different repos or worktrees simultaneously without
82+
path conflicts:
83+
84+
```
85+
construct /home/user/src/projectA # workdir inside container: /home/user/src/projectA
86+
construct /home/user/src/projectB # workdir inside container: /home/user/src/projectB
87+
```
88+
89+
## Persistence
90+
91+
No changes to persistence. Home volume naming (`construct-home-<tool>-<hex8>`,
92+
where `hex8` is SHA256 of the absolute repo path) is unchanged.
93+
94+
## Files changed
95+
96+
| File | Change |
97+
|---|---|
98+
| `docs/spec/path-mirror.md` | This spec |
99+
| `internal/runner/runner.go` | Add `containerWorkdir` helper; replace hardcoded `:/workspace:z` + `-w /workspace` in `buildRunArgs`, `buildServeArgs`, `buildDebugArgs`; inject `CONSTRUCT_WORKSPACE_PATH` env var |
100+
| `internal/runner/runner_test.go` | Tests for `containerWorkdir`; verify `-v` and `-w` flags in build*Args; verify env var injection |
101+
| `CHANGELOG.md` | Entry under `[Unreleased]` |

internal/runner/runner.go

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -353,9 +353,11 @@ func buildRunArgs(cfg *Config, dindInst *dind.Instance, image, sessionID, homeVo
353353
// context into ~/.config/opencode/AGENTS.md.
354354
args = append(args, "-e", "CONSTRUCT_DOCKER_MODE="+cfg.DockerMode)
355355

356+
workdir := containerWorkdir(cfg.RepoPath)
356357
args = append(args,
357-
"-v", cfg.RepoPath+":/workspace:z",
358-
"-w", "/workspace",
358+
"-v", cfg.RepoPath+":"+workdir+":z",
359+
"-w", workdir,
360+
"-e", "CONSTRUCT_WORKSPACE_PATH="+workdir,
359361
)
360362

361363
// Mount the host's global opencode commands directory read-only so the
@@ -484,9 +486,11 @@ func buildServeArgs(cfg *Config, dindInst *dind.Instance, image, sessionID, home
484486

485487
args = append(args, "-e", "CONSTRUCT_DOCKER_MODE="+cfg.DockerMode)
486488

489+
workdir := containerWorkdir(cfg.RepoPath)
487490
args = append(args,
488-
"-v", cfg.RepoPath+":/workspace:z",
489-
"-w", "/workspace",
491+
"-v", cfg.RepoPath+":"+workdir+":z",
492+
"-w", workdir,
493+
"-e", "CONSTRUCT_WORKSPACE_PATH="+workdir,
490494
)
491495

492496
if cfg.Tool.Name == "opencode" {
@@ -589,9 +593,11 @@ func buildDebugArgs(cfg *Config, dindInst *dind.Instance, image, sessionID, home
589593

590594
args = append(args, "-e", "CONSTRUCT_DOCKER_MODE="+cfg.DockerMode)
591595

596+
workdir := containerWorkdir(cfg.RepoPath)
592597
args = append(args,
593-
"-v", cfg.RepoPath+":/workspace:z",
594-
"-w", "/workspace",
598+
"-v", cfg.RepoPath+":"+workdir+":z",
599+
"-w", workdir,
600+
"-e", "CONSTRUCT_WORKSPACE_PATH="+workdir,
595601
)
596602

597603
if cfg.Tool.Name == "opencode" {
@@ -901,21 +907,21 @@ func generatedEntrypoint() string {
901907
"# to inform the agent that it is running inside a construct container.\n" +
902908
"# The networking section depends on CONSTRUCT_DOCKER_MODE.\n" +
903909
"mkdir -p \"${HOME}/.config/opencode\"\n" +
904-
"cat > \"${HOME}/.config/opencode/AGENTS.md\" << 'AGENTSEOF'\n" +
910+
"cat > \"${HOME}/.config/opencode/AGENTS.md\" << AGENTSEOF\n" +
905911
"# Construct container context\n" +
906912
"\n" +
907913
"You are running inside a construct container.\n" +
908914
"\n" +
909915
"## Workspace\n" +
910916
"\n" +
911-
"`/workspace` is the user's repository, bind-mounted from their machine.\n" +
917+
"\\`${CONSTRUCT_WORKSPACE_PATH}\\` is the directory shared with the user,\n" +
918+
"bind-mounted from their machine at its exact host path.\n" +
912919
"Changes you make there are immediately visible to the user.\n" +
913-
"This is the only directory shared with the user.\n" +
914920
"\n" +
915921
"## Isolation\n" +
916922
"\n" +
917-
"Everything outside `/workspace` is isolated inside the container.\n" +
918-
"Your home directory (`/home/agent`) persists across sessions via a named Docker\n" +
923+
"Everything outside the shared directory is isolated inside the container.\n" +
924+
"Your home directory (\\`/home/agent\\`) persists across sessions via a named Docker\n" +
919925
"volume, so shell history, tool caches, and config files survive container\n" +
920926
"restarts. The user's machine is separate — you cannot reach their localhost and\n" +
921927
"they cannot reach yours.\n" +

0 commit comments

Comments
 (0)