Files
utility-scripts/isolate
T
2026-10-16 18:11:57 +02:00

117 lines
3.4 KiB
Bash
Executable File

#!/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}" "${@}"