Safe Repository Access for AI Agents with bwrap and sshfs
I run AI agents in a dedicated VM. The agents need access to my local git repositories so they can read and modify code, but I don’t want them to be able to push to remotes, read my SSH keys, or touch anything outside the repos directory — even if an agent somehow gains root in the VM.
The Setup
The workflow looks like this:
- The host machine holds the git repositories and is where I push and commit.
- The agents VM runs AI agents (e.g. Claude Code). It has no git credentials of its own.
- The agent mounts the repos via sshfs, reads and writes files normally, but has no path to the outside world from the host’s perspective.
This means I retain full control over git history — I review what the agent changed, then push myself. The agent just works on the files.
The Problem with a Plain SSH Key
If I gave the agents VM a normal SSH key, it would have full shell access to my home directory. Even an sftp-only key without a command= restriction can browse the entire filesystem the user has access to. And if the agent gains root inside the VM, it controls that key.
The critical property I want: even root in the agents VM cannot access anything on the host beyond the repos directory. The constraint has to live on the host, not inside the VM.
Solution: Forced Command + bwrap Sandbox
Two mechanisms work together:
authorized_keysforced command — The host runs a wrapper script for this key regardless of what the client requests.bwrapsandbox — The wrapper launchessftp-serverinside a bubblewrap namespace where only the repos directory is visible, read-write.
The Wrapper Script
Save this as /usr/local/bin/sftp-force-directory on the host:
#!/usr/bin/env bash
# SPDX-License-Identifier: MIT
# Copyright (c) 2026 Jan Christoph Uhde <jan@uhde.io>
set -uo pipefail
log() {
echo "$(date) [$USER] $*" >&2
echo "$(date) [$USER] $*" >> "$HOME/sftp_wrapper.log"
}
if (( $# != 1 )); then
echo "Usage: $0 <allowed-directory>" >&2
exit 2
fi
allowed_dir="$1"
allowed_command="/usr/lib/openssh/sftp-server"
if [[ -z "$allowed_dir" || "$allowed_dir" == '/' || ! -d "$allowed_dir" ]]; then
log "Invalid allowed_dir: '$allowed_dir'"
exit 1
fi
if [[ -z "${SSH_ORIGINAL_COMMAND:-}" ]]; then
log "Access denied: no command provided"
exit 1
fi
log "CMD: $SSH_ORIGINAL_COMMAND"
if [[ "$SSH_ORIGINAL_COMMAND" != "$allowed_command" ]]; then
log "Access denied: only sftp-server is allowed"
exit 1
fi
allowed_real=$(realpath -- "$allowed_dir")
exec bwrap \
--ro-bind /usr /usr \
--ro-bind /lib /lib \
--ro-bind /lib64 /lib64 \
--ro-bind /bin /bin \
--ro-bind /etc/passwd /etc/passwd \
--dir /dev \
--dev-bind /dev/null /dev/null \
--dev-bind /dev/zero /dev/zero \
--dev-bind /dev/random /dev/random \
--dev-bind /dev/urandom /dev/urandom \
--proc /proc \
--dir /tmp \
--bind "$allowed_real" /mnt \
--chdir /mnt \
/usr/lib/openssh/sftp-server
bwrap gives sftp-server read-only access to the system libraries it needs, and read-write access to exactly one directory mounted at /mnt. The rest of the host filesystem simply does not exist inside the sandbox.
authorized_keys Entry on the Host
command="/usr/local/bin/sftp-force-directory /path/to/repos-claude" <your public key> root@agents-vm
The command= option means OpenSSH ignores whatever the client asks for and always runs the wrapper. The target directory is baked into the key entry — different keys can be locked to different directories.
No extra options like no-pty are required: the wrapper itself rejects everything except an exact sftp-server invocation, so a shell can never be opened regardless.
sshfs Mount on the Agents VM
On the agents VM, mount the repos directory via sshfs. An /etc/fstab entry for a persistent mount:
vmhost: /home/user/remote-repos fuse.sshfs _netdev,reconnect,ServerAliveInterval=15,ServerAliveCountMax=3,allow_other,uid=1001,gid=1001 0 0
The agent sees the repos as a normal local filesystem and can read, write, and run git operations on them. It just has no credentials to push anywhere.
What This Protects Against
- No host filesystem access — the agent cannot see anything outside the repos directory, not even a directory listing one level up.
- No shell — only
sftp-serveris accepted; any other command is logged and rejected. - Root in the VM is not enough — even if an agent gains root and controls the SSH key, the host-side wrapper and bwrap sandbox still apply. The kernel enforces the namespace; there is nothing inside the VM that can bypass it.
- No git push from the VM — the agent has no git remote credentials. It can commit locally to the mounted repos, but pushing requires credentials that live only on the host.
Dependencies
bwrap(bubblewrap) —apt install bubblewrapon Debian/Ubuntusshfson the agents VM —apt install sshfs- OpenSSH server with
sftp-serverat/usr/lib/openssh/sftp-server(adjust path if needed)