No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
joe 124dadb0e7 install スクリプトを追加
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>
2026-10-09 17:52:02 +09:00
.gitignore プロジェクト dir の .ssh を本来の home 位置に bind 2026-10-07 19:53:24 +09:00
binds.conf.example エージェント向けサポートを手厚く 2026-10-07 19:25:54 +09:00
devshell プロジェクト dir の .ssh を本来の home 位置に bind 2026-10-07 19:53:24 +09:00
install install スクリプトを追加 2026-10-09 17:52:02 +09:00
README.md install スクリプトを追加 2026-10-09 17:52:02 +09:00

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 前提に依存する

参考