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
2 changes: 1 addition & 1 deletion plugins/me/commands/claude-isolated-test.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ description: Docker 컨테이너에서 Claude Code를 대화형으로 테스트
argument-hint: [-n NAME] [-w DIR] [-s]
---

Use and follow the claude-isolated-test skill exactly as written.
Use and follow the isolated-test-guide skill exactly as written.
90 changes: 83 additions & 7 deletions plugins/me/skills/claude-isolated-test/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
---
name: claude-isolated-test
name: isolated-test-guide
description: Docker 컨테이너에서 Claude Code를 대화형으로 테스트
---

# Claude Isolated Test

격리된 Docker 환경에서 Claude Code를 실행합니다.
격리된 Docker 환경에서 Claude Code를 실행합니다. tmux를 통한 세션 관리와 다양한 인증 방식을 지원합니다.

## 실행

Expand All @@ -15,10 +15,86 @@ description: Docker 컨테이너에서 Claude Code를 대화형으로 테스트

## 옵션

- `-n NAME` - 컨테이너 이름 (default: claude-dev)
- `-w DIR` - 워크스페이스 디렉토리 (default: 현재 디렉토리)
- `-s` - 컨테이너 정지
### 컨테이너 설정

## tmux 단축키
- `-n, --name NAME` - 컨테이너 이름 (default: claude-dev)
- `-i, --image IMAGE` - Docker 이미지 이름 (default: claude-test:latest)
- `-w, --workspace DIR` - 워크스페이스 디렉토리 (default: 현재 디렉토리)

- 떼어내기: `Ctrl+B` → `D`
### tmux 세션 관리

- `-S, --session-name NAME` - tmux 세션 이름 (default: claude)
- `-l, --list-sessions` - 컨테이너 내 활성 tmux 세션 목록 출력
- `-k, --kill-session NAME` - 특정 tmux 세션 종료

### 컨테이너 제어

- `-s, --stop-only` - 컨테이너 정지 및 제거 (attach 없음)
- `-h, --help` - 도움말 표시

## 인증 방식

인증 토큰은 다음 우선순위로 검색됩니다:

1. **ANTHROPIC_API_KEY** - 표준 API 키 (CI/CD 권장)
2. **CLAUDE_CODE_OAUTH_TOKEN** - OAuth 토큰
3. **ANTHROPIC_AUTH_TOKEN** - OAuth 토큰 (대체)
4. **Keychain** - macOS Keychain의 `Claude Code-credentials`

인증 방식이 없으면 오류 메시지와 함께 설정 방법을 안내합니다.

## tmux 통합

### 세션 동작

- 컨테이너에 접속하면 tmux 세션이 자동으로 생성되거나 기존 세션에 연결됩니다
- 세션은 컨테이너 외부의 Docker 볼륨에 저장되어 컨테이너 재시작 후에도 유지됩니다
- 여러 tmux 세션을 동시에 실행할 수 있습니다

### tmux 단축키

- 떼어내기 (detach): `Ctrl+B` → `D`
- 세션 목록: `Ctrl+B` → `s`
- 새 창: `Ctrl+B` → `c`
- 창 전환: `Ctrl+B` → `0-9`

## 볼륨 마운트

- **/workspace** - 워크스페이스 디렉토리 (읽기/쓰기)
- **/tmux** - tmux 소켓 저장소 (Docker named volume)

tmux 소켓은 별도의 볼륨으로 저장되어 컨테이너 재시작 간 세션 상태가 유지됩니다.

## 사용 예시

### 기본 실행

```bash
./shell.sh
```

### 여러 세션 사용

```bash
# 첫 번째 세션
./shell.sh --session-name backend

# 두 번째 세션 (별도 터미널)
./shell.sh --session-name frontend

# 세션 목록 확인
./shell.sh --list-sessions

# 특정 세션 종료
./shell.sh --kill-session frontend
```

### 컨테이너 관리

```bash
# 컨테이너 정지
./shell.sh --stop-only

# API 키 사용
ANTHROPIC_API_KEY=sk-ant-xxx ./shell.sh
```
5 changes: 5 additions & 0 deletions plugins/me/skills/claude-isolated-test/docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ WORKDIR /workspace
RUN useradd -m -s /bin/bash claude && \
chown -R claude:claude /workspace

# tmux 소켓 영속성을 위한 디렉토리 추가
ENV TMUX_TMPDIR=/tmux
RUN mkdir -p /tmux && \
chown -R claude:claude /tmux

# Switch to non-root user
USER claude

Expand Down
128 changes: 108 additions & 20 deletions plugins/me/skills/claude-isolated-test/shell.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ set -euo pipefail
# ===== CONFIGURATION =====
IMAGE_NAME="${CLAUDE_TEST_IMAGE:-claude-test:latest}"
CONTAINER_NAME="${CLAUDE_CONTAINER_NAME:-claude-dev}"
SESSION_NAME="${CLAUDE_SESSION_NAME:-claude}"
WORKSPACE="${WORKSPACE:-$(pwd)}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

Expand All @@ -12,6 +13,25 @@ check_docker_available() {
docker info &>/dev/null
}

kill_session() {
local container_name="$1"
local session_name="$2"

if ! container_running "$container_name"; then
echo "Error: Container $container_name is not running" >&2
return 1
fi

# 세션 존재 여부 확인
if ! docker exec "$container_name" tmux list-sessions 2>/dev/null | grep -q "^${session_name}:"; then
echo "Error: Session '$session_name' not found in container $container_name" >&2
return 1
fi

echo "Killing tmux session: $session_name" >&2
docker exec "$container_name" tmux kill-session -t "$session_name"
}

build_claude_image() {
local image_name="$1"
local docker_dir="$2"
Expand All @@ -36,11 +56,12 @@ container_running() {
create_interactive_container() {
local container_name="$1"
local image_name="$2"
local oauth_token="$3"
local workspace="${4:-$(pwd)}"
local auth_token="$3"
local auth_type="${4:-oauth}"
local workspace="${5:-$(pwd)}"

if [ -z "$oauth_token" ] || [ "$oauth_token" = "null" ]; then
echo "Error: OAuth token is empty" >&2
if [ -z "$auth_token" ] || [ "$auth_token" = "null" ]; then
echo "Error: Authentication token is empty" >&2
return 1
fi

Expand All @@ -54,24 +75,47 @@ create_interactive_container() {
docker rm "$container_name" &>/dev/null || true
fi

local env_var="-e CLAUDE_CODE_OAUTH_TOKEN"
if [ "$auth_type" = "api-key" ]; then
env_var="-e ANTHROPIC_API_KEY"
fi

docker run -d \
--name "$container_name" \
-e CLAUDE_CODE_OAUTH_TOKEN="$oauth_token" \
"$env_var"="$auth_token" \
-v "$workspace:/workspace" \
-v "${container_name}-tmux:/tmux" \
-e TMUX_TMPDIR=/tmux \
-w /workspace \
"$image_name" \
sleep infinity
}

attach_container_tmux() {
attach_container_claude() {
local container_name="$1"
local session_name="${2:-claude}"

if ! container_running "$container_name"; then
echo "Error: Container $container_name is not running" >&2
return 1
fi

# tmux 세션 attach 또는 생성 (-A 플래그)
docker exec -it "$container_name" tmux new-session -A -s "$session_name"
}

list_sessions() {
local container_name="$1"

if ! container_running "$container_name"; then
echo "Error: Container $container_name is not running" >&2
return 1
fi

docker exec -it "$container_name" tmux new-session -A -s claude
if ! docker exec "$container_name" tmux list-sessions 2>/dev/null; then
echo "No active tmux sessions in container: $container_name" >&2
return 1
fi
}

# ===== MAIN LOGIC =====
Expand All @@ -81,27 +125,55 @@ usage() {
echo "Attach to a Claude Code Docker container with tmux for interactive development." >&2
echo "" >&2
echo "Options:" >&2
echo " -n, --name NAME Container name (default: claude-dev)" >&2
echo " -i, --image IMAGE Docker image name (default: claude-test:latest)" >&2
echo " -w, --workspace DIR Workspace directory to mount (default: current dir)" >&2
echo " -s, --stop-only Stop and remove the container without attaching" >&2
echo " -h, --help Show this help" >&2
echo " -n, --name NAME Container name (default: claude-dev)" >&2
echo " -i, --image IMAGE Docker image name (default: claude-test:latest)" >&2
echo " -w, --workspace DIR Workspace directory to mount (default: current dir)" >&2
echo " -S, --session-name NAME Tmux session name (default: claude)" >&2
echo " -l, --list-sessions List active tmux sessions in the container" >&2
echo " -k, --kill-session NAME Kill a specific tmux session in the container" >&2
echo " -s, --stop-only Stop and remove the container without attaching" >&2
echo " -h, --help Show this help" >&2
exit 1
}

# Parse arguments
STOP_ONLY=false
LIST_SESSIONS=false
KILL_SESSION=""
while [[ $# -gt 0 ]]; do
case $1 in
-n|--name) CONTAINER_NAME="$2"; shift 2 ;;
-i|--image) IMAGE_NAME="$2"; shift 2 ;;
-w|--workspace) WORKSPACE="$2"; shift 2 ;;
-S|--session-name) SESSION_NAME="$2"; shift 2 ;;
-l|--list-sessions) LIST_SESSIONS=true; shift ;;
-k|--kill-session) KILL_SESSION="$2"; shift 2 ;;
-s|--stop-only) STOP_ONLY=true; shift ;;
-h|--help) usage ;;
*) echo "Unknown option: $1" >&2; usage ;;
esac
done

# List-sessions mode
if [ "$LIST_SESSIONS" = true ]; then
if ! check_docker_available; then
echo "Error: Docker is not available or not running" >&2
exit 1
fi
list_sessions "$CONTAINER_NAME"
exit $?
fi

# Kill-session mode
if [ -n "$KILL_SESSION" ]; then
if ! check_docker_available; then
echo "Error: Docker is not available or not running" >&2
exit 1
fi
kill_session "$CONTAINER_NAME" "$KILL_SESSION"
exit $?
fi

# Stop-only mode
if [ "$STOP_ONLY" = true ]; then
if container_exists "$CONTAINER_NAME"; then
Expand All @@ -127,21 +199,37 @@ if ! docker image inspect "$IMAGE_NAME" &>/dev/null; then
build_claude_image "$IMAGE_NAME" "$SCRIPT_DIR/docker"
fi

# Get OAuth token
if [ -n "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]; then
# Get authentication token (priority order)
AUTH_TYPE=""

if [ -n "${ANTHROPIC_API_KEY:-}" ]; then
TOKEN="$ANTHROPIC_API_KEY"
AUTH_TYPE="api-key"
echo "Using API key from ANTHROPIC_API_KEY environment variable" >&2
elif [ -n "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]; then
TOKEN="$CLAUDE_CODE_OAUTH_TOKEN"
echo "Using OAuth token from environment" >&2
AUTH_TYPE="oauth"
echo "Using OAuth token from CLAUDE_CODE_OAUTH_TOKEN environment variable" >&2
elif [ -n "${ANTHROPIC_AUTH_TOKEN:-}" ]; then
TOKEN="$ANTHROPIC_AUTH_TOKEN"
AUTH_TYPE="oauth"
echo "Using OAuth token from ANTHROPIC_AUTH_TOKEN environment variable" >&2
elif command -v security &>/dev/null; then
echo "Getting OAuth token from Keychain..." >&2
TOKEN=$(security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null | jq -r '.claudeAiOauth.accessToken' || echo "")
AUTH_TYPE="oauth"
else
echo "Warning: No OAuth token found. Set CLAUDE_CODE_OAUTH_TOKEN environment variable." >&2
echo "Warning: No authentication token found." >&2
TOKEN=""
fi

if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
echo "Error: OAuth token not available." >&2
echo "Set CLAUDE_CODE_OAUTH_TOKEN environment variable or ensure Keychain has 'Claude Code-credentials'." >&2
echo "Error: Authentication token not available." >&2
echo "Set one of the following environment variables:" >&2
echo " - ANTHROPIC_API_KEY (standard API key, recommended for CI/CD)" >&2
echo " - CLAUDE_CODE_OAUTH_TOKEN (OAuth token)" >&2
echo " - ANTHROPIC_AUTH_TOKEN (OAuth token alternative)" >&2
echo "Or ensure Keychain has 'Claude Code-credentials'." >&2
exit 1
fi

Expand All @@ -152,12 +240,12 @@ echo "Image: $IMAGE_NAME"
echo "Workspace: $WORKSPACE"
echo "========================================" >&2

create_interactive_container "$CONTAINER_NAME" "$IMAGE_NAME" "$TOKEN" "$WORKSPACE"
create_interactive_container "$CONTAINER_NAME" "$IMAGE_NAME" "$TOKEN" "$AUTH_TYPE" "$WORKSPACE"

echo "" >&2
echo "Attaching to container with tmux..." >&2
echo "Press Ctrl+B then D to detach without stopping the container" >&2
echo "Run '$0 --stop-only' to stop and remove the container" >&2
echo "" >&2

attach_container_tmux "$CONTAINER_NAME"
attach_container_claude "$CONTAINER_NAME" "$SESSION_NAME"
Loading