Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions .devcontainer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# .devcontainer

Claude Code をホスト直接実行せず、隔離されたコンテナ内で動かすための devcontainer 設定です。
NaCl Claude Code 利用ガイドライン **階層 B(OSS / 自社開発)** に準拠します。

## 設計方針

- **既存の `docker-compose.yml` の `app` サービスを再利用** する。`docker-compose.devcontainer.yml` で mount のみ追加する override 構成
- ホスト全体ではなく、**作業に必要なディレクトリのみ** をマウントする(物理アクセス範囲の制限)
- 既存の `bin/dx` / `bin/setup-worktree` / git worktree 運用と共存する
- VS Code Dev Containers と devpod の両方で動作する標準準拠

## マウント構成

devcontainer のマウントは **共有用** と **個人用** を 2 つの compose ファイルに
分けている。

### `docker-compose.devcontainer.yml` (commit、全員共通)

| ホスト側 | コンテナ内 | 用途 |
|---|---|---|
| `<repo root>` | `/app` | 作業ディレクトリ(既存 compose と同じ) |
| `~/.gitconfig` | `/root/.gitconfig` (ro) | git ユーザー名・コミット署名 |
| `~/.config/gh` | `/root/.config/gh` | gh CLI 設定 |

### `docker-compose.local.yml` (`.gitignore`、各自で `*.example` からコピー)

ghq の有無、Claude Code の利用有無などはユーザーによって違うため、個人ごとに
override する。`.devcontainer/docker-compose.local.yml.example` をコピーして
自分の環境に合わせる:

```bash
cp .devcontainer/docker-compose.local.yml.example \
.devcontainer/docker-compose.local.yml
# 不要な mount をコメントアウトして使う
```

template には以下がデフォルトで含まれる:

| ホスト側 | コンテナ内 | 用途 |
|---|---|---|
| `~/ghq` | `/ghq` | 関連 OSS の参照・push (使わない人はコメントアウト) |
| `~/.claude.json` | `/root/.claude.json` | Claude Code 認証 |
| `~/.claude/settings.json` | 同上 (ro) | Claude Code グローバル設定 |
| `~/.claude/skills` | 同上 (ro) | 自作 skills |
| `~/.claude/plugins` | 同上 (ro) | プラグイン |
| `~/.claude/statusline-command.sh` | 同上 (ro) | カスタム statusline |
| `~/.claude/projects/-app` | `/root/.claude/projects/-app` | このプロジェクト固有 memory |

`docker-compose.local.yml` が無い場合は `initialize.sh` が空の stub を自動生成
するので、Claude Code を使わない・ghq を使わない人は何もせずそのまま動作する。

**マウントしないもの (全員)**: `~/.ssh`, `~/.aws`, `~/Documents`, `~/Downloads`,
`~/Desktop`, `~/Library`, 他案件ディレクトリ、**`~/.claude/projects/`,
`~/.claude/sessions/`, `~/.claude/history.jsonl`, `~/.claude/file-history/`,
`~/.claude/shell-snapshots/`** (他プロジェクトの転写・履歴の漏れを防ぐ)。

### Claude Code の認証・設定・memory 共有について

`~/.claude` のうち、共有して安全なもの(グローバル設定・自作 skills・認証)だけを bind mount し、**他プロジェクトの transcripts や履歴は意図的に隔離** する設計。

このプロジェクトのメモリ (`~/.claude/projects/<host slug>/memory/`) はホスト側スラッグがユーザーのパスに依存するため、`initialize.sh` がホスト側に **`~/.claude/projects/-app` → `~/.claude/projects/<host slug>`** のシンボリックリンクを作成する。コンテナ内では workspace=`/app` のため slug=`-app` で固定され、リンク経由で同じ memory を読み書きできる。他のマシンや他の worktree に切り替えても、`initialize.sh` がそのときの host slug にリンクを張り直す。

### gh CLI の認証トークンについて (macOS 必須手順)

macOS の `gh` は PAT を **Keychain** に保存するため、`~/.config/gh` を
bind mount しても `oauth_token` がコンテナに渡らない。devcontainer 起動前に
ホスト側のシェルで以下を実行して `GH_TOKEN` 環境変数として注入する:

```bash
export GH_TOKEN=$(gh auth token)
```

devcontainer.json の `remoteEnv.GH_TOKEN: "${localEnv:GH_TOKEN}"` 経由で
コンテナ内 `gh` が PAT を読み取れるようになる。未設定で devcontainer を
起動した場合、`initialize.sh` が警告を出す (devcontainer 自体は起動する)。

## 起動方法

### VS Code Dev Containers

1. VS Code に「Dev Containers」拡張をインストール
2. リポジトリを VS Code で開く
3. コマンドパレットから **Dev Containers: Reopen in Container**

### devpod (CLI)

```bash
# 初回のみ
brew install devpod
devpod provider add docker

# 起動
cd <repo root>
devpod up .

# Claude Code をコンテナ内で起動
devpod ssh smalruby3-editor -- claude

# 停止
devpod stop smalruby3-editor
```

### docker compose 直接 (CLI only)

```bash
# devcontainer のオーバーレイを適用して起動
docker compose -f docker-compose.yml -f .devcontainer/docker-compose.devcontainer.yml up -d app

# コンテナに入る
docker compose -f docker-compose.yml -f .devcontainer/docker-compose.devcontainer.yml exec app bash
```

## 既存ワークフローへの影響

| 既存仕組み | 影響 |
|---|---|
| `docker compose run --rm app ...` | 変更なし。devcontainer を立てなくても従来通り動く |
| `bin/dx` | 変更なし(devcontainer 内では二重 docker を避けるラッパーを後続フェーズで検討) |
| `bin/setup-worktree` | `.devcontainer/post-create.sh` 経由で **worktree のときだけ** 実行(main checkout では skip) |
| git worktree | compose の `name: smalruby3-editor` 固定で named volume を共有 |

## AWS CDK deploy について

`~/.aws` はマウントしません。`cdk deploy` は人間が diff を見て発動する操作なので、
**ホスト側で実行** することを推奨します。コンテナ内では `cdk synth` / `cdk diff` まで(AWS 認証不要)。

必要時のみ短命 STS token を環境変数で注入する運用も可能(要検討)。

## トラブルシューティング

- **gh auth が効かない**: macOS では PAT が keychain に格納されるため、`export GH_TOKEN=$(gh auth token)` を実行してから devcontainer を起動する (上記「gh CLI の認証トークンについて」参照)
- **`/ghq` が空**: `ghq root` の出力が `~/ghq` 以外を指していないか確認
- **ポート 8601 が見えない**: VS Code は `forwardPorts` で自動転送。devpod / 直接 compose では `docker-compose.yml` の `ports` 設定で `localhost:8601` に公開される
14 changes: 14 additions & 0 deletions .devcontainer/devcontainer-lock.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"features": {
"ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {
"version": "1.0.5",
"resolved": "ghcr.io/anthropics/devcontainer-features/claude-code@sha256:cfc2e7d3e9fd3b9b01f8d5cb158508a884c8c0ede2e23ed10f32dea5d4ffe69a",
"integrity": "sha256:cfc2e7d3e9fd3b9b01f8d5cb158508a884c8c0ede2e23ed10f32dea5d4ffe69a"
},
"ghcr.io/devcontainers/features/github-cli:1": {
"version": "1.1.0",
"resolved": "ghcr.io/devcontainers/features/github-cli@sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671",
"integrity": "sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671"
}
}
}
31 changes: 31 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "smalruby3-editor (Claude Code)",
"dockerComposeFile": [
"../docker-compose.yml",
"docker-compose.devcontainer.yml",
"docker-compose.local.yml"
],
"service": "app",
"workspaceFolder": "/app",
"overrideCommand": false,
"features": {
"ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {},
"ghcr.io/devcontainers/features/github-cli:1": {}
},
"remoteEnv": {
"GHQ_ROOT": "/ghq",
"GH_TOKEN": "${localEnv:GH_TOKEN}"
},
"forwardPorts": [8601],
"initializeCommand": ".devcontainer/initialize.sh",
"postCreateCommand": ".devcontainer/post-create.sh",
"customizations": {
"vscode": {
"extensions": [
"anthropic.claude-code",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}
}
}
14 changes: 14 additions & 0 deletions .devcontainer/docker-compose.devcontainer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
services:
app:
# devcontainer 用に長寿命化(既存 compose は `npm start` で短命)
command: sleep infinity
volumes:
# 全開発者で共通のマウント。個人依存のもの (~/ghq, ~/.claude 等) は
# docker-compose.local.yml に置く (gitignore、各自で .example からコピー)。
- type: bind
source: ${HOME}/.gitconfig
target: /root/.gitconfig
read_only: true
- type: bind
source: ${HOME}/.config/gh
target: /root/.config/gh
55 changes: 55 additions & 0 deletions .devcontainer/docker-compose.local.yml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# .devcontainer/docker-compose.local.yml
#
# 個人ごとに devcontainer を拡張するための override。`.gitignore` 対象。
# 使い方: このファイルを `docker-compose.local.yml` にコピーし、自分の環境に
# 合わせて編集する。
#
# cp .devcontainer/docker-compose.local.yml.example \
# .devcontainer/docker-compose.local.yml
#
# devcontainer.json はこのファイルを dockerComposeFile に含めるため、Reopen
# in Container すると自動で適用される。空のままでも害はない。
#
# 注意:
# - bind mount のソースが存在しないと devcontainer の起動が失敗するため、
# 不要なエントリはコメントアウトしておく。
# - Claude Code を使わない人は claude セクションを丸ごと省略してよい。
# - ghq を使わない人は ghq セクションを省略してよい。

services:
app:
volumes:
# --- ghq (関連 OSS の参照) ---
# 自分の ghq root が ~/ghq でない場合は source を書き換える。
- type: bind
source: ${HOME}/ghq
target: /ghq

# --- Claude Code: グローバル設定・認証のみ共有 ---
# 他プロジェクトの transcripts / sessions / history は意図的に
# マウントしないことで漏れを防ぐ。詳細は README.md 参照。
- type: bind
source: ${HOME}/.claude.json
target: /root/.claude.json
- type: bind
source: ${HOME}/.claude/settings.json
target: /root/.claude/settings.json
read_only: true
- type: bind
source: ${HOME}/.claude/skills
target: /root/.claude/skills
read_only: true
- type: bind
source: ${HOME}/.claude/plugins
target: /root/.claude/plugins
read_only: true
- type: bind
source: ${HOME}/.claude/statusline-command.sh
target: /root/.claude/statusline-command.sh
read_only: true
# このプロジェクトの memory のみ共有。initialize.sh が
# ${HOME}/.claude/projects/-app を host slug へのシンボリックリンクと
# して作成する。
- type: bind
source: ${HOME}/.claude/projects/-app
target: /root/.claude/projects/-app
77 changes: 77 additions & 0 deletions .devcontainer/initialize.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/env bash
# .devcontainer/initialize.sh
#
# Runs on the host before the devcontainer starts (devcontainer.json の
# initializeCommand から呼ばれる)。以下を行う:
#
# 1. docker-compose.local.yml が無ければ空の stub を作る
# (devcontainer.json は常にこのファイルを参照するため)
# 2. GH_TOKEN が export されていなければ案内を出す
# (macOS の gh は PAT を keychain に保存するため、~/.config/gh を
# bind mount しただけではコンテナ内 gh から認証できない)
# 3. ホスト側に ~/.claude/projects/-app シンボリックリンクを作成
# (Claude Code を使う人向けの per-project memory bridge)

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

# --- 1) docker-compose.local.yml の stub 作成 -----------------------------
LOCAL_COMPOSE="${SCRIPT_DIR}/docker-compose.local.yml"
if [[ ! -f "${LOCAL_COMPOSE}" ]]; then
cat >"${LOCAL_COMPOSE}" <<'EOF'
# Auto-generated by .devcontainer/initialize.sh.
# 個人ごとの mount を追加したい場合は
# .devcontainer/docker-compose.local.yml.example を参考に書き換える。
# このファイルは .gitignore 対象なので、個人の変更は他開発者に共有されない。
services:
app: {}
EOF
echo "initialize: created empty ${LOCAL_COMPOSE} (edit to add personal mounts)"
fi

# --- 2) GH_TOKEN チェック -------------------------------------------------
if [[ -z "${GH_TOKEN:-}" ]]; then
if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then
cat <<'MSG' >&2
warning: GH_TOKEN is not set in the host shell.
The devcontainer mounts ~/.config/gh, but on macOS the token is stored in
the keychain and is not available inside the container.

To enable `gh` inside the devcontainer, run before launching:

export GH_TOKEN=$(gh auth token)

Then start/reopen the devcontainer.
MSG
else
echo "warning: host gh CLI not authenticated. devcontainer-internal gh will be unusable." >&2
fi
fi

# --- 3) Claude Code per-project memory bridge ----------------------------
# Claude Code をホスト側にインストールしていないユーザは何もしない。
# このセクションは本人が docker-compose.local.yml で
# ~/.claude/projects/-app マウントを有効にしている場合に意味を持つ。
if [[ -d "${HOME}/.claude/projects" ]]; then
WORKSPACE_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
HOST_PROJECT_SLUG="$(printf '%s' "${WORKSPACE_DIR}" | sed 's:/:-:g')"
HOST_PROJECT_DIR="${HOME}/.claude/projects/${HOST_PROJECT_SLUG}"
ALIAS_DIR="${HOME}/.claude/projects/-app"

mkdir -p "${HOST_PROJECT_DIR}/memory"

if [[ -L "${ALIAS_DIR}" ]]; then
CURRENT_TARGET="$(readlink "${ALIAS_DIR}")"
if [[ "${CURRENT_TARGET}" != "${HOST_PROJECT_DIR}" ]]; then
rm "${ALIAS_DIR}"
ln -s "${HOST_PROJECT_DIR}" "${ALIAS_DIR}"
fi
elif [[ -e "${ALIAS_DIR}" ]]; then
echo "warning: ${ALIAS_DIR} exists as a non-symlink; not touching it" >&2
else
ln -s "${HOST_PROJECT_DIR}" "${ALIAS_DIR}"
fi
fi

exit 0
32 changes: 32 additions & 0 deletions .devcontainer/post-create.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# .devcontainer/post-create.sh
#
# Runs inside the container after creation (devcontainer.json の
# postCreateCommand から呼ばれる)。
#
# - worktree であれば bin/setup-worktree を実行 (env コピー + npm install + build:dev)
# - main checkout であれば既存の node_modules / dist / .env がそのままあるので
# 何もしない (husky hooks も既にインストール済みのはず)

set -euo pipefail

cd /app

GIT_COMMON_DIR=$(git rev-parse --git-common-dir 2>/dev/null || echo "")
GIT_DIR=$(git rev-parse --git-dir 2>/dev/null || echo "")

if [[ -z "${GIT_COMMON_DIR}" ]]; then
echo "post-create: not a git repository, skipping setup"
exit 0
fi

GIT_COMMON_ABS=$(cd "${GIT_COMMON_DIR}" && pwd)
GIT_DIR_ABS=$(cd "${GIT_DIR}" && pwd)

if [[ "${GIT_COMMON_ABS}" == "${GIT_DIR_ABS}" ]]; then
echo "post-create: main checkout detected, no worktree setup needed"
exit 0
fi

echo "post-create: worktree detected, running bin/setup-worktree"
exec bin/setup-worktree
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,6 @@ infra/**/cdk.out.*/
infra/**/tmp/
infra/**/.cdk.staging/
infra/**/cdk-out-*/

# devcontainer の個人用 mount override (initialize.sh が空 stub を生成する)
.devcontainer/docker-compose.local.yml
Loading