Skip to content

Support mounting host directories outside workspace - #83

Draft
janost wants to merge 1 commit into
jingkaihe:mainfrom
janost:direct-mount-outside-workspace
Draft

Support mounting host directories outside workspace#83
janost wants to merge 1 commit into
jingkaihe:mainfrom
janost:direct-mount-outside-workspace

Conversation

@janost

@janost janost commented Mar 4, 2026

Copy link
Copy Markdown

Summary

This is a PR for #82.

Add --direct-mount flag to mount host directories at arbitrary guest paths, independent of --workspace. Also fix FUSE-mounted file ownership to reflect host UID/GID instead of always showing root:root.

Motivation

All VFS mounts currently must be subpaths of --workspace. This makes it impossible to mount host credentials or config at locations tools expect them (e.g., ~/.ssh, ~/.aws, ~/.config/gh) without setting up a workspace that encompasses them. Direct mounts solve this by allowing workspace-independent host-to-guest directory mapping.

Usage

matchlock run --image myimage \
  --direct-mount ~/.ssh:/home/agent/.ssh \
  --direct-mount ~/.aws:/home/agent/.aws:rw \
  -v "$(pwd)":"$(pwd)":host_fs --workspace "$(pwd)" \
  -- bash -l
  • Read-only by default, append :rw for read-write
  • Works with or without --workspace — direct-mount-only sandboxes are valid

Changes

API layer (pkg/api/)

  • mount.go: DirectMountSpec type and ParseDirectMountSpec() parser — handles host:guest[:rw] format with host path validation, guest path safety checks, and absolute path resolution
  • errors.go: ErrInvalidDirectMountFormat sentinel error
  • config.go: HasVFSMounts() now considers direct mounts; ValidateVFS() validates direct mount guest paths independently of workspace; interception check accepts direct mounts
  • mount_test.go: 7 tests for ParseDirectMountSpec (basic readonly, explicit rw, host not exist, relative guest, invalid format, unknown option, too many parts)
  • config_test.go: 4 tests (direct mounts without workspace, mixed mounts, invalid path rejection, HasVFSMounts with direct mounts only)

CLI (cmd/matchlock/)

  • cmd_run.go: --direct-mount repeatable flag, viper binding, parsing into vfsConfig.DirectMounts — VFS config now created when direct mounts are present even without workspace

Sandbox (pkg/sandbox/)

  • sandbox_common.go: buildVFSProviders() creates RealFSProvider/ReadonlyProvider for each direct mount; new sortedDirectMountPaths() helper for deterministic kernel arg ordering
  • sandbox_linux.go / sandbox_darwin.go: Pass sortedDirectMountPaths(config) to VMConfig

VM backends (pkg/vm/)

  • backend.go: DirectMountPaths []string added to VMConfig
  • linux/backend.go: Adds matchlock.direct_mount=path1,path2 kernel arg
  • darwin/backend.go: Same kernel arg added to all 3 format string variants (no-network, interception, default)

Guest FUSE daemon (internal/guestruntime/fused/)

  • main.go: getDirectMountsFromCmdline() parses matchlock.direct_mount= kernel arg; connectVFSClient() extracted as helper; Run() refactored to collect mount points from both workspace and direct mounts, create one VFSClient + FUSE server per mount, and use sync.WaitGroup for concurrent serve/shutdown

SDK (pkg/sdk/)

  • types.go: DirectMountConfig struct; DirectMounts field on CreateOptions
  • builder.go: MountDirect() and MountDirectReadonly() builder methods
  • create.go: Serializes direct_mounts in VFS params of create request

Bug fix: VFS UID/GID propagation (pkg/vfs/server.go + fused/main.go)

  • VFSStat wire format now includes UID/GID fields
  • statFromInfo() extracts UID/GID from syscall.Stat_t on the host side
  • Guest FUSE fillAttr() passes UID/GID through to the kernel via fuse.Attr
  • Fixes all FUSE-mounted files showing as root:root — affects workspace mounts too, not just direct mounts

Notes

  • Written with Claude Code with human guidance.
  • I manually verified it works, but I was only able to test it on a Linux host.
  • Test are passing.
  • The current implementation is only able to mount directories. I'm still working on mounting single files.

@janost
janost marked this pull request as draft March 5, 2026 17:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant