Skip to content

Instantly share code, notes, and snippets.

@digitalsignalperson
Last active March 20, 2026 18:32
Show Gist options
  • Select an option

  • Save digitalsignalperson/9f8c995dd7c0f61f36b432d003edd5b3 to your computer and use it in GitHub Desktop.

Select an option

Save digitalsignalperson/9f8c995dd7c0f61f36b432d003edd5b3 to your computer and use it in GitHub Desktop.
Isolated vscode environments for claude code with bubblewrap
#!/bin/bash
{
cat > /dev/null << 'EOF'
This is a bubblewrap script I use to isolate claude-code from my personal computer
Environment:
- CODE_SESSION_STORAGE: Required storage location for session home folders
- CODE_CONFIG_BASENAME: The vscode ~/.config location name. Defaults to "Code - OSS". Depends on linux distribution
- CODE_EXTENSIONS_PATH: e.g. a file you can provide like `code --list-extensions > ~/.config/vscode-extensions.txt`
that will be loaded in the container's vscode
Requires:
- Uses gum for simple TUI prompts
- Assumes using wayland, but easy to modify for X11
Usage:
,bwrap-code-session.sh <path to project>
Behaviour:
- Takes the basename of the project as the environment name (must be globally unique amongst your containers)
- The home folder in the container is persisted to CODE_SESSION_STORAGE/{project basname}
- On startup, prompted to choose persistent or ephemeral session
- If persistent and a session with the basename already exists, options to continue, exit, or reset.
- Copies your local vscode config (e.g. ~/.config/Code - OSS/User) into the container
- Any settings changes in the container stay in the container
- Installs optional list of extensions on startup
- Bind mounts r/w the project path provided in the input arg into the container under the same path
- In the container, sets the default browser to a simple clipboard stub: Any link that is opened is copied to clipboard and a notification pops up
TODO:
- add network proxy https://gist.github.com/digitalsignalperson/c4c14a00572c0f4ee028a474b103a80b
EOF
if [ -z "$1" ]; then
echo "Must specify a path to open with code to bind it in the sandbox"
exit 1
fi
if [ -z "CODE_SESSION_STORAGE" ]; then
echo "Must specify CODE_SESSION_STORAGE for where to store bwrap sessions"
exit 1
fi
CODE_CONFIG_BASENAME="${CODE_CONFIG_BASENAME:-"Code - OSS"}"
CODE_EXTENSIONS_PATH="${CODE_EXTENSIONS_PATH:-$(mktemp --tmpdir code_extensions.XXXXXX)}"
# e.g. maintain a custom ~/.config/vscode-extensions.txt
# code --list-extensions > ~/.config/vscode-extensions.txt
src_dir="$(realpath "$1")"
name="$(basename "$src_dir")"
config_dir="$(mktemp -d)"
# make a persistent session based on the basename
session_folder=""
cleanup_folder=""
echo "Select session type:"
choice=$(gum choose "persistent" "tmpfs")
case $choice in
"persistent")
session_folder="$CODE_SESSION_STORAGE/${name}"
if [ -e "$session_folder" ]; then
echo "Session folder with name \"$name\" already exists. Select action:"
action=$(gum choose "continue" "exit" "reset")
case $action in
"continue")
;;
"exit")
exit 0
;;
"reset")
if ! gum confirm "Delete the pre-existing session folder?"; then
exit 0
fi
rm -rf "$session_folder"
;;
*)
exit 1
;;
esac
fi
mkdir -p "$session_folder"
echo "Using persistent session folder $session_folder"
sleep 0.5
;;
"tmpfs")
session_folder="$(mktemp -d --suffix="code-session_$name")"
cleanup_folder="$session_folder"
echo "Created tmpfs session folder $session_folder"
sleep 0.5
;;
*)
exit 1
;;
esac
config_dir="$session_folder"/.config
# Copy config
rsync -a --mkpath \
~/.config/"$CODE_CONFIG_BASENAME"/User/ \
"$session_folder"/.config/"$CODE_CONFIG_BASENAME"/User/
cp "$CODE_EXTENSIONS_PATH" "$session_folder"/.config/vscode-extensions.txt
# Ensure cc extension is included
echo "anthropic.claude-code" >> "$session_folder"/.config/vscode-extensions.txt
# Copy gitconfig
cp ~/.gitconfig "$session_folder"
# Make a launch.sh entrypoint
script=$(mktemp)
cat > "$script" << 'EOF'
#!/bin/bash
# See dbus solution solution from https://stackoverflow.com/questions/68869085/how-to-make-chrome-use-session-bus-instead-of-system-bus
# make dbus visible to other processes
mkdir -p /var/run/dbus/
export DBUS_STARTER_BUS_TYPE="session"
export DBUS_STARTER_ADDRESS="unix:path=/var/run/dbus/system_bus_socket"
export DBUS_SESSION_BUS_ADDRESS="unix:path=/var/run/dbus/system_bus_socket"
unset DBUS_SESSION_BUS_PID
unset DBUS_SESSION_BUS_WINDOWID
# open unix socket for dbus, on common setups it's done by systemd
python -c "import socket; s = socket.socket(socket.AF_UNIX); s.bind('/var/run/dbus/system_bus_socket')"
# start it
dbus-daemon --session --nofork --nosyslog --nopidfile --address=$DBUS_STARTER_ADDRESS >> /tmp/dbus.log 2>&1 &
returncode=$?
DBUS_PID=$!
[ $returncode -ne 0 ] && exit $returncode
# echo -e "[startup] dbus started with PID $DBUS_PID"
cat > ~/.config/electron-flags.conf << FOE
--enable-features=WaylandWindowDecorations
--ozone-platform-hint=auto
FOE
# If these flags are given as args or put in code-flags.conf
# it prints annoying "Warning: 'flag' is not in the list of known options, but still passed to Electron/Chromium.
cat ~/.config/vscode-extensions.txt | while read extension || [[ -n $extension ]];
do
code --install-extension $extension
done
mkdir -p ~/.local/bin
cat > ~/.local/bin/clipboard-browser << FOE
#!/bin/bash
url="\$1"
echo -n "\$url" | wl-copy
kdialog --msgbox "Copied URL to clipboard: \$url"
FOE
chmod +x ~/.local/bin/clipboard-browser
mkdir -p ~/.local/share/applications;
cat > ~/.local/share/applications/clipboard-browser.desktop << FOE
[Desktop Entry]
Version=1.0
Type=Application
Exec=$HOME/.local/bin/clipboard-browser %U
Name=clipboard-browser
FOE
cat > ~/.local/share/applications/mimeapps.list << 'FOE'
[Default Applications]
x-scheme-handler/http=clipboard-browser.desktop
x-scheme-handler/https=clipboard-browser.desktop
x-scheme-handler/ftp=clipboard-browser.desktop
text/html=clipboard-browser.desktop
application/xhtml+xml=clipboard-browser.desktop
application/xhtml_xml=clipboard-browser.desktop
application/rdf+xml=clipboard-browser.desktop
application/rss+xml=clipboard-browser.desktop
application/xml=clipboard-browser.desktop
text/xml=clipboard-browser.desktop
image/gif=clipboard-browser.desktop
image/jpeg=clipboard-browser.desktop
image/png=clipboard-browser.desktop
image/webp=clipboard-browser.desktop
application/pdf=clipboard-browser.desktop
x-scheme-handler/mailto=clipboard-browser.desktop
FOE
# --password-store is to avoid annoying kwallet prompts. can't be in electron-flags.conf, only works here
code \
--password-store=basic \
--wait \
--verbose \
"$1"
returncode=$?
kill $DBUS_PID
exit $returncode
EOF
chmod +x "$script"
# For x11 add
# --ro-bind /tmp/.X11-unix /tmp/.X11-unix \
bwrap \
--dir /usr/local \
--symlink usr/bin /bin \
--symlink usr/bin /sbin \
--symlink usr/lib /lib \
--symlink usr/lib64 /lib64 \
--ro-bind /usr/bin /usr/bin \
--ro-bind /usr/lib /usr/lib \
--ro-bind /usr/lib64 /usr/lib64 \
--ro-bind /usr/share /usr/share \
--ro-bind /usr/include /usr/include \
--ro-bind /etc /etc \
--tmpfs /tmp \
--proc /proc \
--dev /dev \
--dev-bind /dev/dri /dev/dri \
--ro-bind /sys/dev/char /sys/dev/char \
--ro-bind /sys/devices /sys/devices \
--dir "$XDG_RUNTIME_DIR" \
--ro-bind "$XDG_RUNTIME_DIR/wayland-0" "$XDG_RUNTIME_DIR/wayland-0" \
--ro-bind "$XDG_RUNTIME_DIR/pipewire-0" "$XDG_RUNTIME_DIR/pipewire-0" \
--ro-bind "$XDG_RUNTIME_DIR/pulse" "$XDG_RUNTIME_DIR/pulse" \
--ro-bind /run/systemd/resolve/stub-resolv.conf /run/systemd/resolve/stub-resolv.conf \
--unshare-all \
--share-net \
--die-with-parent \
--new-session \
--bind "$src_dir" "$src_dir" \
--bind "$session_folder" "$HOME" \
--chdir $HOME \
--setenv TZ "America/Vancouver" \
--dir "$XDG_RUNTIME_DIR/doc" \
--dev-bind /dev/fuse /dev/fuse \
--ro-bind "$script" "$HOME/launch.sh" \
-- $HOME/launch.sh "$src_dir"
if [ -d "$cleanup_folder" ]; then
if gum confirm "Remove tmpfs session folder? $cleanup_folder" --timeout=10s; then
rm -rf "$cleanup_folder"
fi
fi
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment