From d579c1208007658ca50759e5e5206136db8e11a1 Mon Sep 17 00:00:00 2001 From: Timothy Allen Date: Fri, 16 Oct 2026 18:11:57 +0200 Subject: [PATCH] Harden isolate --- isolate | 96 ++++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 68 insertions(+), 28 deletions(-) diff --git a/isolate b/isolate index afc4b0c..2e6c2d3 100755 --- a/isolate +++ b/isolate @@ -1,34 +1,44 @@ -#/bin/bash +#!/bin/bash -project_dir=$( pwd ) +project_dir=$( pwd -P ) || exit 1 +home_dir=$( readlink -e -- "${HOME}" ) || exit 1 -bubblewrap=$( command -v bwrap ) +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}" ]]; then - echo "Not running in ${HOME}" - exit +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 - args="${@}" else - echo "Please specify a coding agent" - exit + echo "Please specify a coding agent" >&2 + exit 1 fi if ! [[ " ${agents[@]} " =~ " ${coder} " ]]; then - echo "No agent found" - exit + echo "No agent found" >&2 + exit 1 fi declare -a ro_binds -declare -a binds +declare -a rw_binds +declare -a bwrap_args +declare -a bind_sources +declare -a bind_destinations ro_binds=( /usr @@ -40,32 +50,62 @@ ro_binds=( /etc/ssl /etc/passwd /etc/group + "${HOME}/sources" ) -for loc in "${HOME}/.gitconfig" "${HOME}/.local"; do - if [[ -e "${loc}" ]]; then - ro_binds+=("${loc}") +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 -binds=() - -for loc in "${HOME}/.claude" "${HOME}/.pi" "${project_dir}"; do - if [[ -e "${loc}" ]]; then - binds+=("${loc}") +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 -for i in "${!ro_binds[@]}"; do - ro_binds["${i}"]="--ro-bind ${ro_binds[${i}]} ${ro_binds[${i}]}" +# 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 i in "${!binds[@]}"; do - binds["${i}"]="--bind ${binds[${i}]} ${binds[${i}]}" +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 -${bubblewrap} \ - ${ro_binds[@]} \ - ${binds[@]} \ +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 \ @@ -73,4 +113,4 @@ ${bubblewrap} \ --unshare-pid \ --die-with-parent \ --chdir "${project_dir}" \ - "$( command -v ${coder} )" "${args}" + "${coder}" "${@}"