#!/bin/bash project_dir=$( pwd -P ) || exit 1 home_dir=$( readlink -e -- "${HOME}" ) || exit 1 if ! bubblewrap=$( command -v bwrap ); then echo "bubblewrap (bwrap) is required" >&2 exit 1 fi agents=( $( command -v claude ) $( command -v codex ) $( command -v pi ) $( command -v ps ) # for testing $( command -v ls ) # for testing $( command -v touch ) # for testing $( command -v bash ) # for testing ) if [[ "${project_dir}" = "${home_dir}" ]]; then echo "Not running in ${HOME}" >&2 exit 1 fi if [[ -n "${1}" ]] && [[ -n $( command -v "${1}" ) ]]; then coder=$( command -v "${1}" ) && shift else echo "Please specify a coding agent" >&2 exit 1 fi if ! [[ " ${agents[@]} " =~ " ${coder} " ]]; then echo "No agent found" >&2 exit 1 fi declare -a ro_binds declare -a rw_binds declare -a bwrap_args declare -a bind_sources declare -a bind_destinations ro_binds=( /usr /lib /lib64 /bin /etc/resolv.conf /etc/hosts /etc/ssl /etc/passwd /etc/group "${HOME}/sources" ) for readonly_path in "${HOME}/.gitconfig" "${HOME}/bin" "${HOME}/.local" "${HOME}/.atuin" "${XDG_RUNTIME_DIR}"; do if [[ -e "${readonly_path}" ]]; then ro_binds+=("${readonly_path}") fi done for readwrite_path in "${HOME}/.claude" "${HOME}/.claude.json" "${HOME}/.local/share/claude" "${HOME}/.pi" "${project_dir}"; do if [[ -e "${readwrite_path}" ]]; then rw_binds+=("${readwrite_path}") fi done # Resolve mount sources, but keep the paths applications expect as destinations. for readonly_path in "${ro_binds[@]}"; do bind_source=$( readlink -e -- "${readonly_path}" ) || exit 1 bwrap_args+=("--ro-bind" "${bind_source}" "${readonly_path}") bind_sources+=("${bind_source}") bind_destinations+=("${readonly_path}") done for readwrite_path in "${rw_binds[@]}"; do bind_source=$( readlink -e -- "${readwrite_path}" ) || exit 1 bwrap_args+=("--bind" "${bind_source}" "${readwrite_path}") bind_sources+=("${bind_source}") bind_destinations+=("${readwrite_path}") done git_config="${project_dir}/.git/config" git_worktree_config="${project_dir}/.git/config.worktree" if [[ -f "${project_dir}/.git" ]]; then # Linked worktrees keep their shared and per-worktree config elsewhere. git_config=$( git -C "${project_dir}" rev-parse --path-format=absolute --git-path config ) || exit 1 git_worktree_config=$( git -C "${project_dir}" rev-parse --path-format=absolute --git-path config.worktree ) || exit 1 fi # Mask secrets after all directory mounts so later binds cannot expose them. for hide_path in "${git_config}" "${git_worktree_config}" "${project_dir}/.env"; do if [[ -e "${hide_path}" ]]; then hide_path=$( readlink -e -- "${hide_path}" ) || exit 1 bwrap_args+=("--ro-bind" /dev/null "${hide_path}") # A symlinked mount can expose the same file at another destination. for i in "${!bind_sources[@]}"; do if [[ "${hide_path}" = "${bind_sources[${i}]}" || "${hide_path}" = "${bind_sources[${i}]}/"* ]]; then mask_path="${bind_destinations[${i}]}${hide_path#"${bind_sources[${i}]}"}" if [[ "${mask_path}" != "${hide_path}" ]]; then bwrap_args+=("--ro-bind" /dev/null "${mask_path}") fi fi done fi done "${bubblewrap}" \ "${bwrap_args[@]}" \ --tmpfs /tmp \ --proc /proc \ --dev /dev \ --share-net \ --unshare-pid \ --die-with-parent \ --chdir "${project_dir}" \ "${coder}" "${@}"