- Shell 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
bwrap の導入・~/.local/bin への symlink・binds.conf の初期作成を 冪等に行う。既存 binds.conf は上書きしない。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> |
||
| .gitignore | ||
| binds.conf.example | ||
| devshell | ||
| install | ||
| README.md | ||
devshell
devshell に入ったらカレントディレクトリが home になっていた件 〜exit するまでが異世界開発〜
bwrap (bubblewrap) 製の軽量プロジェクト砂箱。
起動したディレクトリが砂箱内の HOME になる。中で書いたものは
その dir に残り、exit で現実に戻る。
他の home 配下は不可視、ホストのツールはそのまま使える。
AI エージェントや怪しいスクリプトに作業範囲を限定する用途向け。
導入
git clone ssh://git.cyberius.biz/joe/devshell.git ~/code/devshell
~/code/devshell/install # bwrap の導入・symlink・binds.conf 作成までやる
手動なら:
sudo apt install bubblewrap
ln -s ~/code/devshell/devshell ~/.local/bin/devshell
cp binds.conf.example ~/.devshells/binds.conf # 必要に応じて編集
使い方
cd ~/some/project
devshell # 対話シェル (cwd = プロジェクト dir)
devshell . make test # コマンドだけ実行
devshell ~/other/proj # 他の dir を指定
bwrap (bubblewrap) 使い方メモ
低レベルの sandbox ツール。Flatpak の中身がこれ。user/mount namespace を 使って「見せるファイル・書ける場所・namespace 分離」を宣言的に組み立てる。 chroot/proot と違って ptrace 不要で速く、コンテナほど重くない。
bwrap [オプション...] -- コマンド [引数...]
基本概念
- bwrap は常に user + mount namespace を作る (非特権で動く)
- 引数は書いた順に適用される。後から mount したものは前のを上書きできる
- bind 先のパスが無ければ bwrap が勝手に掘る (親dir含む)
- コマンド終了時に namespace ごと消える。 tmpfs の中身も消える
ファイルシステム系オプション
| オプション | 意味 |
|---|---|
--bind SRC DST |
SRC を DST に rw bind |
--ro-bind SRC DST |
read-only bind |
--ro-bind-try SRC DST |
SRC が無ければスキップする ro-bind |
--dev-bind SRC DST |
bind + デバイスアクセス許可 (/dev 用) |
--tmpfs DST |
新しい tmpfs を mount (元の中身を隠す) |
--dir DST |
空ディレクトリを作る |
--symlink SRC DST |
シンボリックリンクを作る |
--proc DST |
新しい /proc を mount |
--dev DST |
最小限の /dev を mount |
--chdir DIR |
起動時の cwd |
--setenv K V / --unsetenv K |
環境変数の設定/削除 |
代表的な2つの設計パターン:
# A. 最小rootfs型 — 必要なものだけ見せる (強い隔離)
bwrap \
--ro-bind /usr /usr \
--symlink usr/bin /bin --symlink usr/lib /lib --symlink usr/lib64 /lib64 \
--ro-bind /etc /etc \
--proc /proc --dev /dev --tmpfs /tmp \
--bind "$PWD" /work --chdir /work \
-- bash
# B. 反転型 — 全部見せてユーザ領域だけ隠す (ツールがそのまま動いて楽)
bwrap \
--ro-bind / / \
--tmpfs /home --tmpfs /tmp --tmpfs /var/tmp \
--proc /proc --dev /dev \
--bind "$PWD" "$PWD" --chdir "$PWD" \
-- bash
devshell (~/.local/bin/devshell) は B 方式 + HOME 差し替え。
devshell の構成
~/.local/bin/devshell… 本体スクリプト- 砂箱内の HOME はプロジェクト dir そのもの。
cdするとプロジェクトに戻る。 書き込みが永続化されるのはこの dir のみ (tmpfs の /home・/tmp や、 ro-bind された場所に書いたものは残らない)。 bind したパス (~/.config/xxx等) は$HOME/<相対>= プロジェクト dir 直下に見える ~/.devshells/<プロジェクト名>/… 旧世代の devhome 跡地 (bind されない)。 残っていても害はないが、不要ならホスト側で消してよい~/.devshells/binds.conf… 砂箱に引き込むパスのホワイトリスト。ro|rw <パス>を1行ずつ。$HOME/<相対>(= プロジェクト dir 直下) と 実パスの両方に bind される (実パスにも bind するのは絶対パス symlink を切らないため)
砂箱内であることの検知: DEVSHELL=1 と container=devshell が環境変数に
立つ (後者は flatpak/podman と同じ慣例で systemd-detect-virt 系も拾える)。
対話シェルのプロンプトは [devshell] になる。
設定や認証 (~/.config/xxx 等) は基本的にここに rw で列挙して共有する。
プロジェクト固有の SSH: プロジェクト dir に .ssh/ を置くと砂箱内で
本来の home 位置 (getpwuid の home) にも bind され、ssh/git が
普通に見つける (OpenSSH は $HOME ではなく passwd の home を見るため)。
ホストの ~/.ssh は持ち込まれない。known_hosts は砂箱内で書かれて
プロジェクト側に残る。鍵の誤コミットに注意 — .gitignore 必須。
注意: bind のマウントポイント確保のため、ホスト側のプロジェクト dir に
.config/ .claude/ .gitconfig 等の空 dir/ファイルが作られる。
git status に出るのが嫌なら .gitignore に追加するか、
スナップショット的に持ち込みたい場合はプロジェクト内に直接 cp -a してもよい
(その場合ホスト側には反映されない)。
namespace 分離オプション
| オプション | 意味 |
|---|---|
--unshare-net |
ネットワーク遮断 (loopback すら無い) |
--unshare-pid |
PID namespace (中のプロセスだけ見える) |
--unshare-ipc |
SysV IPC / POSIX MQ 分離 |
--unshare-uts |
hostname 分離 (これを付けると --hostname 使える) |
--unshare-cgroup |
cgroup namespace |
--unshare-all |
全部 (net も切れるので開発には向かない) |
--share-net |
--unshare-all 後に net だけ戻す |
--hostname NAME |
uts ns 内の hostname |
--uid N / --gid N |
中での uid/gid (fake root なら 0) |
--die-with-parent |
親プロセスが死んだら中も殺す (ほぼ常に付ける) |
よくあるレシピ
# ネット遮断で怪しいビルドスクリプトを試す
bwrap --ro-bind / / --tmpfs /home --tmpfs /tmp --proc /proc --dev /dev \
--unshare-net --unshare-pid --die-with-parent -- bash
# 中で fake root (uid 0) になる
bwrap --ro-bind / / --tmpfs /home --proc /proc --dev /dev \
--uid 0 --gid 0 -- bash
# 一時ディレクトリを HOME にしてクリーン環境でテスト
bwrap --ro-bind / / --tmpfs /home --dir /home/test \
--setenv HOME /home/test --proc /proc --dev /dev -- bash
# 特定ファイルだけを隠す (bind で上書き)
bwrap --ro-bind / / --ro-bind /dev/null /etc/secret.conf -- bash
ハマりどころ
- mount 順序は引数順:
--tmpfs /homeの後に--bind X /home/user/projすれば tmpfs 上に骨格だけ掘ってプロジェクトだけ見える。逆順だと隠れる - bind 先を掘る副作用:
--bindの DST の親が tmpfs でなく実 dir だと、 ホスト側に空のマウントポイント dir が残る。devshell では HOME=プロジェクト dir にしているため、binds.conf の各エントリの分だけプロジェクト dir に.config/.claude/等の空 dir が作られる (git には空 dir は載らないが.gitconfigのような空ファイルは untracked として見える) - 環境変数は素通し:
PATHに~/.local/binがあっても /home を隠せば 中では解決しない。ツールが見えない時は PATH と bind の矛盾を疑う --dev /devは最小限: null/zero/random/tty 程度。GPU 等が要るなら--dev-bind /dev /devか個別に--dev-bind /dev/dri /dev/dri- PID1 問題: bwrap 自身が PID 1 になり、子がゾンビ化することがある。
--unshare-pid+ 長時間プロセスを中で育てる運用で出る。基本は短命な コマンド実行用途と割り切る - セキュリティ境界ではない: 共有カーネルだし
/runのソケット経由で ホスト側サービス (dockerd 等) に話せる。悪意あるコードの完全封じ込め用途 には使わない - snap アプリは中で動かないことが多い: snapd の mount 前提に依存する
参考
man bwrap/bwrap --help- 実装例:
~/.local/bin/devshell - 本家: https://github.com/containers/bubblewrap