Last active
March 20, 2026 18:32
-
-
Save digitalsignalperson/9f8c995dd7c0f61f36b432d003edd5b3 to your computer and use it in GitHub Desktop.
Isolated vscode environments for claude code with bubblewrap
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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