Last active
April 2, 2026 15:39
-
-
Save jclement/7a02d8f1979a1c37085bd3ecc3fceb17 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # megaclaude — Sandboxed Claude Code development environments | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| IMAGE_NAME="megaclaude" | |
| IMAGE_TAG="latest" | |
| HOME_VOLUME="megaclaude-home" | |
| # ── Colors & Style ─────────────────────────────────────────────────────────── | |
| if [[ -t 1 ]]; then | |
| BOLD='\033[1m' | |
| DIM='\033[2m' | |
| RESET='\033[0m' | |
| RED='\033[1;31m' | |
| GREEN='\033[1;32m' | |
| YELLOW='\033[1;33m' | |
| BLUE='\033[1;34m' | |
| MAGENTA='\033[1;35m' | |
| CYAN='\033[1;36m' | |
| WHITE='\033[1;37m' | |
| GRAY='\033[0;37m' | |
| else | |
| BOLD='' DIM='' RESET='' RED='' GREEN='' YELLOW='' | |
| BLUE='' MAGENTA='' CYAN='' WHITE='' GRAY='' | |
| fi | |
| # ── Pretty Printing ───────────────────────────────────────────────────────── | |
| banner() { | |
| echo "" | |
| echo -e "${MAGENTA} ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓${RESET}" | |
| echo -e "${MAGENTA} ┃${RESET} ${BOLD}${WHITE} ⚡ M E G A C L A U D E ⚡${RESET} ${MAGENTA}┃${RESET}" | |
| echo -e "${MAGENTA} ┃${RESET} ${DIM}${GRAY} Sandboxed Claude Code environments${RESET} ${MAGENTA}┃${RESET}" | |
| echo -e "${MAGENTA} ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛${RESET}" | |
| echo "" | |
| } | |
| info() { echo -e " ${CYAN}▸${RESET} $*"; } | |
| success() { echo -e " ${GREEN}✔${RESET} $*"; } | |
| warn() { echo -e " ${YELLOW}⚠${RESET} $*"; } | |
| error() { echo -e " ${RED}✖${RESET} $*"; } | |
| step() { echo -e " ${BLUE}◆${RESET} ${BOLD}$*${RESET}"; } | |
| # ── Usage ──────────────────────────────────────────────────────────────────── | |
| usage() { | |
| banner | |
| echo -e " ${BOLD}Usage:${RESET}" | |
| echo -e " ${WHITE}megaclaude${RESET} ${CYAN}<project-dir>${RESET} ${DIM}[options]${RESET}" | |
| echo "" | |
| echo -e " ${BOLD}Options:${RESET}" | |
| echo -e " ${CYAN}--rebuild${RESET} Full rebuild: image + volumes (nuclear option)" | |
| echo -e " ${CYAN}--resume ${DIM}<id>${RESET} Resume a previous Claude session" | |
| echo -e " ${CYAN}--shell${RESET} Drop into bash instead of launching Claude" | |
| echo -e " ${CYAN}--usage${RESET} Show token usage & cost for all sessions" | |
| echo -e " ${CYAN}--help${RESET} Show this help" | |
| echo "" | |
| echo -e " ${BOLD}Tmux shortcuts${RESET} ${DIM}(inside session):${RESET}" | |
| echo -e " ${CYAN}ctrl+B c${RESET} New Claude Code worker" | |
| echo -e " ${CYAN}ctrl+B C${RESET} New shell" | |
| echo -e " ${CYAN}ctrl+B |${RESET} Split pane horizontal" | |
| echo -e " ${CYAN}ctrl+B -${RESET} Split pane vertical" | |
| echo -e " ${CYAN}ctrl+B n/p${RESET} Next/previous window" | |
| echo "" | |
| echo -e " ${BOLD}Examples:${RESET}" | |
| echo -e " ${DIM}$ megaclaude ./my-project${RESET}" | |
| echo -e " ${DIM}$ megaclaude /path/to/project --rebuild${RESET}" | |
| echo -e " ${DIM}$ megaclaude ./my-project --resume abc123${RESET}" | |
| echo -e " ${DIM}$ megaclaude ./my-project --shell${RESET}" | |
| echo -e " ${DIM}$ megaclaude --usage${RESET}" | |
| echo "" | |
| echo -e " ${BOLD}Volumes:${RESET}" | |
| echo -e " ${DIM}${HOME_VOLUME}${RESET} ${DIM}— persists claude, mise, and auth${RESET}" | |
| echo -e " ${DIM}Authenticate once, reuse across all projects.${RESET}" | |
| echo "" | |
| exit 0 | |
| } | |
| # ── Parse Args ─────────────────────────────────────────────────────────────── | |
| PROJECT_DIR="" | |
| FORCE_REBUILD=false | |
| SHELL_MODE=false | |
| USAGE_MODE=false | |
| RESUME_ID="" | |
| EXPECT_RESUME=false | |
| for arg in "$@"; do | |
| if $EXPECT_RESUME; then | |
| RESUME_ID="$arg" | |
| EXPECT_RESUME=false | |
| continue | |
| fi | |
| case "$arg" in | |
| --rebuild) FORCE_REBUILD=true ;; | |
| --shell) SHELL_MODE=true ;; | |
| --usage) USAGE_MODE=true ;; | |
| --resume) EXPECT_RESUME=true ;; | |
| --help|-h) usage ;; | |
| -*) error "Unknown option: ${arg}"; usage >&2; exit 1 ;; | |
| *) | |
| if [[ -z "$PROJECT_DIR" ]]; then | |
| PROJECT_DIR="$arg" | |
| else | |
| error "Multiple project directories specified" | |
| exit 1 | |
| fi | |
| ;; | |
| esac | |
| done | |
| if [[ -z "$PROJECT_DIR" ]] && ! $USAGE_MODE; then | |
| usage | |
| fi | |
| # ── Usage Mode (no project dir needed) ────────────────────────────────────── | |
| if $USAGE_MODE; then | |
| banner | |
| if ! docker image inspect "${IMAGE_NAME}:${IMAGE_TAG}" &>/dev/null; then | |
| error "No megaclaude image found — run a session first" | |
| exit 1 | |
| fi | |
| if ! docker volume inspect "$HOME_VOLUME" &>/dev/null; then | |
| error "No home volume found — no sessions to report" | |
| exit 1 | |
| fi | |
| HOST_USER="$(id -un)" | |
| docker run --rm \ | |
| -v "${HOME_VOLUME}:/home/${HOST_USER}:ro" \ | |
| -e "DEV_USER=${HOST_USER}" \ | |
| "${IMAGE_NAME}:${IMAGE_TAG}" \ | |
| --usage | |
| exit $? | |
| fi | |
| # Resolve to absolute path | |
| PROJECT_DIR="$(cd "$PROJECT_DIR" 2>/dev/null && pwd)" || { | |
| error "Directory not found: ${PROJECT_DIR}" | |
| exit 1 | |
| } | |
| PROJECT_NAME="$(basename "$PROJECT_DIR")" | |
| banner | |
| # ── Preflight ──────────────────────────────────────────────────────────────── | |
| step "Preflight checks" | |
| if ! command -v docker &>/dev/null; then | |
| error "Docker is not installed or not in PATH" | |
| exit 1 | |
| fi | |
| if ! docker info &>/dev/null; then | |
| error "Docker daemon is not running" | |
| exit 1 | |
| fi | |
| success "Docker is ready" | |
| HOST_UID="$(id -u)" | |
| HOST_GID="$(id -g)" | |
| HOST_USER="$(id -un)" | |
| info "Project: ${BOLD}${PROJECT_NAME}${RESET} ${DIM}→ /workspace${RESET}" | |
| info "User: ${BOLD}${HOST_USER}${RESET} ${DIM}(uid=${HOST_UID} gid=${HOST_GID})${RESET}" | |
| # ── Build Image ────────────────────────────────────────────────────────────── | |
| image_exists() { | |
| docker image inspect "${IMAGE_NAME}:${IMAGE_TAG}" &>/dev/null | |
| } | |
| build_image() { | |
| step "Building Docker image" | |
| echo "" | |
| local build_dir | |
| build_dir=$(mktemp -d) | |
| cat > "${build_dir}/Dockerfile" <<'DOCKERFILE' | |
| FROM ubuntu:24.04 | |
| ARG DEBIAN_FRONTEND=noninteractive | |
| # ── System packages ────────────────────────────────────────────────────── | |
| RUN apt-get update && apt-get install -y --no-install-recommends \ | |
| bc \ | |
| build-essential \ | |
| ca-certificates \ | |
| cmake \ | |
| curl \ | |
| file \ | |
| git \ | |
| gnupg \ | |
| jq \ | |
| less \ | |
| libssl-dev \ | |
| locales \ | |
| man-db \ | |
| openssh-client \ | |
| pkg-config \ | |
| python3 \ | |
| python3-pip \ | |
| python3-venv \ | |
| ripgrep \ | |
| software-properties-common \ | |
| sudo \ | |
| tmux \ | |
| tree \ | |
| unzip \ | |
| vim \ | |
| wget \ | |
| zip \ | |
| zlib1g-dev \ | |
| && rm -rf /var/lib/apt/lists/* | |
| # ── Helix editor ──────────────────────────────────────────────────────── | |
| RUN HELIX_VERSION=$(curl -fsSL https://api.github.com/repos/helix-editor/helix/releases/latest | jq -r .tag_name) \ | |
| && curl -fsSL "https://github.com/helix-editor/helix/releases/download/${HELIX_VERSION}/helix-${HELIX_VERSION}-x86_64-linux.tar.xz" \ | |
| -o /tmp/helix.tar.xz \ | |
| && mkdir -p /opt/helix \ | |
| && tar xf /tmp/helix.tar.xz -C /opt/helix --strip-components=1 \ | |
| && ln -s /opt/helix/hx /usr/local/bin/hx \ | |
| && rm /tmp/helix.tar.xz | |
| # ── Locale ─────────────────────────────────────────────────────────────── | |
| RUN locale-gen en_US.UTF-8 | |
| ENV LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 | |
| # ── Workspace ──────────────────────────────────────────────────────────── | |
| RUN mkdir -p /workspace | |
| # ── Entrypoint (runs as root, creates user, drops privileges) ──────────── | |
| COPY entrypoint.sh /entrypoint.sh | |
| RUN chmod +x /entrypoint.sh | |
| WORKDIR /workspace | |
| ENTRYPOINT ["/entrypoint.sh"] | |
| DOCKERFILE | |
| cat > "${build_dir}/entrypoint.sh" <<'ENTRYPOINT' | |
| #!/usr/bin/env bash | |
| set -e | |
| DEV_UID="${DEV_UID:-1000}" | |
| DEV_GID="${DEV_GID:-1000}" | |
| DEV_USER="${DEV_USER:-dev}" | |
| HOME_DIR="/home/$DEV_USER" | |
| # ── Cost calculation ──────────────────────────────────────────────────────── | |
| # Model pricing per million tokens (input, cache-write, cache-read, output) | |
| calc_cost() { | |
| local jsonl_files=("$@") | |
| if [[ ${#jsonl_files[@]} -eq 0 ]]; then | |
| echo "0 0 0 0 0.00" | |
| return | |
| fi | |
| # Extract per-model usage, output one line per model + a total line | |
| cat "${jsonl_files[@]}" 2>/dev/null | jq -r ' | |
| select(.type == "assistant" and .message.usage != null) | | |
| .message.model as $model | | |
| .message.usage | | |
| # Pricing per million tokens: [input, cache_write, cache_read, output] | |
| (if ($model | test("opus")) then | |
| [15, 18.75, 1.50, 75] | |
| elif ($model | test("haiku")) then | |
| [0.80, 1.00, 0.08, 4] | |
| else | |
| [3, 3.75, 0.30, 15] | |
| end) as $prices | | |
| # Normalize model name | |
| (if ($model | test("opus")) then "opus" | |
| elif ($model | test("haiku")) then "haiku" | |
| else "sonnet" end) as $tier | | |
| (.input_tokens // 0) as $in | | |
| (.cache_creation_input_tokens // 0) as $cw | | |
| (.cache_read_input_tokens // 0) as $cr | | |
| (.output_tokens // 0) as $out | | |
| {tier: $tier, in: $in, cw: $cw, cr: $cr, out: $out, | |
| cost: (($in * $prices[0] + $cw * $prices[1] + $cr * $prices[2] + $out * $prices[3]) / 1000000)} | |
| ' 2>/dev/null | jq -sr ' | |
| group_by(.tier) | | |
| map({ | |
| tier: .[0].tier, | |
| in: (map(.in) | add // 0), | |
| cw: (map(.cw) | add // 0), | |
| cr: (map(.cr) | add // 0), | |
| out: (map(.out) | add // 0), | |
| cost: (map(.cost) | add // 0) | |
| }) | | |
| . as $models | | |
| ($models | map(.in) | add // 0) as $tin | | |
| ($models | map(.cw) | add // 0) as $tcw | | |
| ($models | map(.cr) | add // 0) as $tcr | | |
| ($models | map(.out) | add // 0) as $tout | | |
| ($models | map(.cost) | add // 0) as $tcost | | |
| ($models | map("\(.tier) \(.in) \(.cw) \(.cr) \(.out) \(.cost)") | join("\n")) + | |
| "\ntotal \($tin) \($tcw) \($tcr) \($tout) \($tcost)" | |
| ' 2>/dev/null || echo "total 0 0 0 0 0.00" | |
| } | |
| format_tokens() { | |
| local n=${1%%.*} # truncate to integer | |
| n=${n:-0} | |
| if [[ $n -ge 1000000 ]]; then | |
| printf "%.1fM" "$(echo "$n / 1000000" | bc -l)" | |
| elif [[ $n -ge 1000 ]]; then | |
| printf "%.1fK" "$(echo "$n / 1000" | bc -l)" | |
| else | |
| echo "$n" | |
| fi | |
| } | |
| print_cost_summary() { | |
| local label="$1" | |
| shift | |
| local cost_output="$*" | |
| # Parse model lines and total line | |
| local models=() total_line="" | |
| while IFS= read -r line; do | |
| [[ -z "$line" ]] && continue | |
| local tier="${line%% *}" | |
| if [[ "$tier" == "total" ]]; then | |
| total_line="$line" | |
| else | |
| models+=("$line") | |
| fi | |
| done <<< "$cost_output" | |
| # Parse total | |
| local t_in t_cw t_cr t_out t_cost | |
| read -r _ t_in t_cw t_cr t_out t_cost <<< "$total_line" | |
| t_in=${t_in%%.*}; t_in=${t_in//[^0-9]/}; t_in=${t_in:-0} | |
| t_cw=${t_cw%%.*}; t_cw=${t_cw//[^0-9]/}; t_cw=${t_cw:-0} | |
| t_cr=${t_cr%%.*}; t_cr=${t_cr//[^0-9]/}; t_cr=${t_cr:-0} | |
| t_out=${t_out%%.*}; t_out=${t_out//[^0-9]/}; t_out=${t_out:-0} | |
| t_cost=$(printf "%.2f" "${t_cost:-0}") | |
| local t_total=$((t_in + t_cw + t_cr + t_out)) | |
| echo "" | |
| echo -e " \033[1;37m$label\033[0m" | |
| # Per-model breakdown | |
| for mline in "${models[@]}"; do | |
| local tier m_in m_cw m_cr m_out m_cost | |
| read -r tier m_in m_cw m_cr m_out m_cost <<< "$mline" | |
| m_in=${m_in%%.*}; m_in=${m_in//[^0-9]/}; m_in=${m_in:-0} | |
| m_cw=${m_cw%%.*}; m_cw=${m_cw//[^0-9]/}; m_cw=${m_cw:-0} | |
| m_cr=${m_cr%%.*}; m_cr=${m_cr//[^0-9]/}; m_cr=${m_cr:-0} | |
| m_out=${m_out%%.*}; m_out=${m_out//[^0-9]/}; m_out=${m_out:-0} | |
| m_cost=$(printf "%.2f" "${m_cost:-0}") | |
| local m_total=$((m_in + m_cw + m_cr + m_out)) | |
| printf " \033[1;35m%-10s\033[0m \033[1;36m%10s\033[0m tokens \033[1;32m\$%s\033[0m\n" "$tier" "$(format_tokens $m_total)" "$m_cost" | |
| done | |
| # Show totals if more than one model | |
| if [[ ${#models[@]} -gt 1 ]]; then | |
| echo -e " \033[0;37m────────────────────────────────────\033[0m" | |
| printf " \033[1;37m%-10s\033[0m \033[1;36m%10s\033[0m tokens \033[1;32m\$%s\033[0m\n" "total" "$(format_tokens $t_total)" "$t_cost" | |
| fi | |
| echo "" | |
| } | |
| # ── Usage mode (read-only, no user setup needed) ──────────────────────────── | |
| if [[ "${1:-}" == "--usage" ]]; then | |
| CLAUDE_DIR="$HOME_DIR/.claude" | |
| if [[ ! -d "$CLAUDE_DIR" ]]; then | |
| echo -e "\033[1;31m ✖ No Claude data found\033[0m" | |
| exit 1 | |
| fi | |
| echo "" | |
| echo -e "\033[1;35m ⚡ MEGACLAUDE USAGE REPORT\033[0m" | |
| # Per-session breakdown | |
| SESSION_COUNT=0 | |
| GRAND_INPUT=0 GRAND_CW=0 GRAND_CR=0 GRAND_OUT=0 GRAND_COST=0 | |
| ALL_MODEL_LINES="" | |
| for session_file in "$CLAUDE_DIR"/projects/*/[0-9a-f]*.jsonl; do | |
| [[ -f "$session_file" ]] || continue | |
| SESSION_COUNT=$((SESSION_COUNT + 1)) | |
| session_id=$(basename "$session_file" .jsonl) | |
| session_dir=$(dirname "$session_file")/${session_id} | |
| # Gather main + subagent files | |
| files=("$session_file") | |
| if [[ -d "${session_dir}/subagents" ]]; then | |
| for sf in "${session_dir}/subagents"/*.jsonl; do | |
| [[ -f "$sf" ]] && files+=("$sf") | |
| done | |
| fi | |
| cost_output="$(calc_cost "${files[@]}")" | |
| # Extract total line for the per-session list | |
| local total_line | |
| total_line=$(echo "$cost_output" | grep "^total " || echo "total 0 0 0 0 0.00") | |
| read -r _ s_in s_cw s_cr s_out s_cost <<< "$total_line" | |
| # Get session timestamp & first user message | |
| ts=$(jq -r 'select(.type == "user") | .timestamp' "$session_file" 2>/dev/null | head -1) | |
| first_msg=$(jq -r 'select(.type == "user" and .message.role == "user") | .message.content' "$session_file" 2>/dev/null | head -c 60 | head -1) | |
| [[ ${#first_msg} -ge 59 ]] && first_msg="${first_msg}..." | |
| date_str="${ts:0:10} ${ts:11:5}" | |
| s_cost_fmt=$(printf "%.2f" "${s_cost:-0}") | |
| printf "\033[0;37m %s\033[0m \033[1;32m\$%-8s\033[0m \033[0;37m%s\033[0m\n" "$date_str" "$s_cost_fmt" "$first_msg" | |
| GRAND_INPUT=$((GRAND_INPUT + ${s_in%%.*})) | |
| GRAND_CW=$((GRAND_CW + ${s_cw%%.*})) | |
| GRAND_CR=$((GRAND_CR + ${s_cr%%.*})) | |
| GRAND_OUT=$((GRAND_OUT + ${s_out%%.*})) | |
| GRAND_COST=$(echo "$GRAND_COST + ${s_cost:-0}" | bc -l) | |
| # Accumulate per-model lines | |
| ALL_MODEL_LINES+=$(echo "$cost_output" | grep -v "^total ")$'\n' | |
| done | |
| # Re-aggregate model lines across all sessions | |
| GRAND_COST_OUTPUT=$(echo "$ALL_MODEL_LINES" | awk ' | |
| NF >= 6 { in_t[$1]+=$2; cw[$1]+=$3; cr[$1]+=$4; out[$1]+=$5; cost[$1]+=$6 } | |
| END { | |
| for (t in cost) printf "%s %d %d %d %d %.6f\n", t, in_t[t], cw[t], cr[t], out[t], cost[t] | |
| tin=0; tcw=0; tcr=0; tout=0; tcost=0 | |
| for (t in cost) { tin+=in_t[t]; tcw+=cw[t]; tcr+=cr[t]; tout+=out[t]; tcost+=cost[t] } | |
| printf "total %d %d %d %d %.6f\n", tin, tcw, tcr, tout, tcost | |
| }') | |
| echo "" | |
| echo -e "\033[0;37m ── $SESSION_COUNT session(s) ──\033[0m" | |
| print_cost_summary "All Sessions Total" "$GRAND_COST_OUTPUT" | |
| exit 0 | |
| fi | |
| # ── Create group & user matching host IDs ──────────────────────────────── | |
| # Remove any existing user/group that conflicts with the host IDs | |
| # (e.g. ubuntu:24.04 ships a default 'ubuntu' user at UID 1000) | |
| EXISTING_USER=$(getent passwd "$DEV_UID" | cut -d: -f1 || true) | |
| if [[ -n "$EXISTING_USER" && "$EXISTING_USER" != "$DEV_USER" ]]; then | |
| userdel "$EXISTING_USER" 2>/dev/null || true | |
| fi | |
| EXISTING_GROUP=$(getent group "$DEV_GID" | cut -d: -f1 || true) | |
| if [[ -n "$EXISTING_GROUP" && "$EXISTING_GROUP" != "$DEV_USER" ]]; then | |
| groupdel "$EXISTING_GROUP" 2>/dev/null || true | |
| fi | |
| # Lower UID/GID minimums for macOS compatibility (macOS UIDs start at 501) | |
| sed -i 's/^UID_MIN.*/UID_MIN 500/' /etc/login.defs 2>/dev/null || true | |
| sed -i 's/^GID_MIN.*/GID_MIN 500/' /etc/login.defs 2>/dev/null || true | |
| if ! getent group "$DEV_GID" >/dev/null 2>&1; then | |
| groupadd -g "$DEV_GID" "$DEV_USER" | |
| fi | |
| if ! id "$DEV_USER" >/dev/null 2>&1; then | |
| useradd -M -u "$DEV_UID" -g "$DEV_GID" -s /bin/bash -d "$HOME_DIR" "$DEV_USER" | |
| fi | |
| echo "$DEV_USER ALL=(ALL) NOPASSWD:ALL" > "/etc/sudoers.d/$DEV_USER" | |
| chmod 440 "/etc/sudoers.d/$DEV_USER" | |
| # ── Ensure home dir ownership (volume may be fresh) ────────────────────── | |
| chown "$DEV_UID:$DEV_GID" "$HOME_DIR" | |
| # ── Bootstrap Claude Code + mise (first run only, cached in volume) ────── | |
| CLAUDE_BIN="$HOME_DIR/.local/bin/claude" | |
| MISE_BIN="$HOME_DIR/.local/bin/mise" | |
| run_as_user() { su - "$DEV_USER" -c "$*"; } | |
| if [[ ! -x "$CLAUDE_BIN" ]]; then | |
| echo "" | |
| echo -e "\033[1;35m⚡\033[0m \033[1mFirst run — bootstrapping tools\033[0m" | |
| echo -e "\033[0;37m Installing Claude Code + mise (~30s)\033[0m" | |
| echo -e "\033[0;37m This is cached in a Docker volume for future runs.\033[0m" | |
| echo "" | |
| echo -e "\033[1;36m [1/2]\033[0m Installing Claude Code..." | |
| run_as_user 'curl -fsSL https://claude.ai/install.sh | bash' 2>&1 | |
| echo -e "\033[1;36m [2/2]\033[0m Installing mise..." | |
| run_as_user 'curl -fsSL https://mise.run | sh' 2>&1 | tail -1 | |
| echo "" | |
| echo -e "\033[1;32m ✔ Bootstrap complete!\033[0m" | |
| echo "" | |
| fi | |
| # ── Ensure claude + mise in bashrc ─────────────────────────────────────── | |
| BASHRC="$HOME_DIR/.bashrc" | |
| MEGACLAUDE_MARKER='# --- megaclaude-env ---' | |
| if ! grep -qF "$MEGACLAUDE_MARKER" "$BASHRC" 2>/dev/null; then | |
| { | |
| echo "$MEGACLAUDE_MARKER" | |
| echo 'export PATH="$HOME/.local/bin:$PATH"' | |
| echo 'eval "$($HOME/.local/bin/mise activate bash)"' | |
| } >> "$BASHRC" | |
| chown "$DEV_UID:$DEV_GID" "$BASHRC" | |
| fi | |
| # ── Fix workspace ownership ───────────────────────────────────────────── | |
| chown "$DEV_UID:$DEV_GID" /workspace | |
| # ── Install project tools if mise config present ───────────────────────── | |
| cd /workspace | |
| SETUP_CMD="" | |
| if [[ -f .mise.toml ]] || [[ -f .tool-versions ]] || [[ -f mise.toml ]]; then | |
| SETUP_CMD="$MISE_BIN trust 2>/dev/null; echo -e '\033[1;35m▸\033[0m Installing project tools...'; $MISE_BIN install --yes 2>/dev/null; " | |
| fi | |
| if [[ -n "$SETUP_CMD" ]]; then | |
| su - "$DEV_USER" -c "cd /workspace && ${SETUP_CMD}true" | |
| fi | |
| # ── Drop privileges and launch ─────────────────────────────────────────── | |
| export HOME="$HOME_DIR" USER="$DEV_USER" LOGNAME="$DEV_USER" | |
| # ── Parse entrypoint args ─────────────────────────────────────────────── | |
| SHELL_MODE=false | |
| RESUME_ID="" | |
| for arg in "$@"; do | |
| case "$arg" in | |
| --shell) SHELL_MODE=true ;; | |
| --resume) :;; # next arg is the ID | |
| *) | |
| # Check if previous arg was --resume | |
| if [[ "${PREV_ARG:-}" == "--resume" ]]; then | |
| RESUME_ID="$arg" | |
| fi | |
| ;; | |
| esac | |
| PREV_ARG="$arg" | |
| done | |
| # Also accept MEGA_RESUME env var | |
| RESUME_ID="${RESUME_ID:-${MEGA_RESUME:-}}" | |
| if $SHELL_MODE; then | |
| exec setpriv --reuid="$DEV_UID" --regid="$DEV_GID" --init-groups bash --login | |
| else | |
| # ── Write CLAUDE.md with mission context (read automatically by Claude) ── | |
| if [[ -n "${MEGA_MISSION:-}" ]]; then | |
| mkdir -p "$HOME_DIR/.claude" | |
| cat > "$HOME_DIR/.claude/CLAUDE.md" <<MISSION_EOF | |
| # MEGACLAUDE SESSION | |
| You are running in a fully sandboxed Docker container with --dangerously-skip-permissions. | |
| You have complete freedom to read, write, execute, and install anything — nothing can break. | |
| Be bold and autonomous. Do not ask for permission or confirmation — just do the work. | |
| If you need to install packages, do it. If you need to create files, do it. | |
| If you need to run tests, run them. If something fails, fix it and retry. | |
| Explore the codebase first to understand what you're working with, then execute completely. | |
| ## Your Mission | |
| ${MEGA_MISSION} | |
| --- | |
| Work autonomously until the mission is fully complete. Show your work as you go. | |
| MISSION_EOF | |
| chown -R "$DEV_UID:$DEV_GID" "$HOME_DIR/.claude" | |
| fi | |
| # ── Build claude launch command ─────────────────────────────────────── | |
| CLAUDE_ARGS="--dangerously-skip-permissions" | |
| if [[ -n "$RESUME_ID" ]]; then | |
| CLAUDE_ARGS="$CLAUDE_ARGS --resume $RESUME_ID" | |
| fi | |
| echo -e "\033[1;35m▸\033[0m Launching Claude Code \033[0;37m(skip-permissions)\033[0m" | |
| if [[ -n "$RESUME_ID" ]]; then | |
| echo -e "\033[1;35m▸\033[0m Resuming session \033[1;36m${RESUME_ID}\033[0m" | |
| fi | |
| echo "" | |
| # Create a timestamp marker so we can find files from this session | |
| CLAUDE_DIR="$HOME_DIR/.claude" | |
| TIMESTAMP_MARKER=$(mktemp -p "${CLAUDE_DIR}" .ts_marker.XXXXXX 2>/dev/null || mktemp) | |
| chown "$DEV_UID:$DEV_GID" "$TIMESTAMP_MARKER" | |
| # ── Write tmux config ─────────────────────────────────────────────────── | |
| TMUX_CONF="$HOME_DIR/.tmux.conf" | |
| cat > "$TMUX_CONF" <<'TMUX_EOF' | |
| set -g default-terminal "screen-256color" | |
| set -ga terminal-overrides ",*256col*:Tc" | |
| set -g mouse on | |
| set -g base-index 1 | |
| setw -g pane-base-index 1 | |
| set -g renumber-windows on | |
| set -sg escape-time 0 | |
| set -g status-interval 1 | |
| set -g status-style bg=colour235,fg=colour255 | |
| set -g status-left-length 50 | |
| set -g status-left "#[fg=colour232,bg=colour39,bold] #S #[fg=colour39,bg=colour235,nobold]" | |
| set -g status-right-length 80 | |
| set -g status-right "#[fg=colour245,bg=colour235] ^b c:claude ^b C:shell #[fg=colour240,bg=colour235]#[fg=colour250,bg=colour240] %Y-%m-%d #[fg=colour252]#[fg=colour16,bg=colour252,bold] %H:%M:%S " | |
| setw -g window-status-format "#[fg=colour245,bg=colour235] #I #[fg=colour245,bg=colour235] #W " | |
| setw -g window-status-current-format "#[fg=colour235,bg=colour39]#[fg=colour232,bg=colour39,bold] #I #W #[fg=colour39,bg=colour235,nobold]" | |
| set -g pane-border-style fg=colour240 | |
| set -g pane-active-border-style fg=colour39,bg=default | |
| set -g pane-border-status top | |
| set -g pane-border-format "#[fg=colour240,bg=colour235] #P: #T " | |
| setw -g pane-active-border-style fg=colour39,bold | |
| setw -g window-active-style bg=terminal | |
| setw -g window-style bg=colour234 | |
| set -g display-panes-colour colour240 | |
| set -g display-panes-active-colour colour39 | |
| set -g message-style bg=colour235,fg=colour39,bold | |
| bind | split-window -h -c "#{pane_current_path}" | |
| bind - split-window -v -c "#{pane_current_path}" | |
| unbind '"' | |
| unbind % | |
| # ctrl+B c = new Claude Code instance | |
| bind c new-window -n "claude" -c /workspace "$HOME/.local/bin/claude --dangerously-skip-permissions" | |
| # ctrl+B C = new shell | |
| bind C new-window -n "shell" -c /workspace | |
| bind h select-pane -L | |
| bind j select-pane -D | |
| bind k select-pane -U | |
| bind l select-pane -R | |
| bind -r H resize-pane -L 5 | |
| bind -r J resize-pane -D 5 | |
| bind -r K resize-pane -U 5 | |
| bind -r L resize-pane -R 5 | |
| bind r source-file ~/.tmux.conf \; display "Reloaded!" | |
| setw -g mode-keys vi | |
| bind -T copy-mode-vi v send -X begin-selection | |
| bind -T copy-mode-vi y send -X copy-selection-and-cancel | |
| TMUX_EOF | |
| chown "$DEV_UID:$DEV_GID" "$TMUX_CONF" | |
| # ── Launch inside tmux ────────────────────────────────────────────────── | |
| # Run claude inside tmux so ctrl+B c spawns additional workers | |
| set +e | |
| setpriv --reuid="$DEV_UID" --regid="$DEV_GID" --init-groups \ | |
| tmux new-session -s megaclaude -n claude \ | |
| "cd /workspace && $CLAUDE_BIN $CLAUDE_ARGS" | |
| CLAUDE_EXIT=$? | |
| set -e | |
| # ── Session cost summary ──────────────────────────────────────────────── | |
| # Find conversation JSONL files created/modified during this session | |
| # (UUID-named files only, excludes history.jsonl etc.) | |
| SESSION_FILES=() | |
| while IFS= read -r f; do | |
| [[ -n "$f" ]] && SESSION_FILES+=("$f") | |
| done < <(find "$CLAUDE_DIR" -name "*.jsonl" -newer "$TIMESTAMP_MARKER" \ | |
| -regex '.*/[0-9a-f].*\.jsonl' 2>/dev/null) | |
| rm -f "$TIMESTAMP_MARKER" | |
| if [[ ${#SESSION_FILES[@]} -gt 0 ]]; then | |
| cost_output="$(calc_cost "${SESSION_FILES[@]}")" | |
| total_cost=$(echo "$cost_output" | grep "^total " | awk '{print $6}') | |
| total_cost=$(printf "%.2f" "${total_cost:-0}") | |
| if [[ "$total_cost" != "0.00" && "$total_cost" != "0" ]]; then | |
| print_cost_summary "Session Cost" "$cost_output" | |
| fi | |
| fi | |
| exit $CLAUDE_EXIT | |
| fi | |
| ENTRYPOINT | |
| docker build -t "${IMAGE_NAME}:${IMAGE_TAG}" "$build_dir" 2>&1 | while IFS= read -r line; do | |
| if [[ "$line" =~ ^Step ]]; then | |
| echo -e " ${CYAN}${line}${RESET}" | |
| elif [[ "$line" =~ ^"Successfully" ]]; then | |
| echo -e " ${GREEN}${line}${RESET}" | |
| elif [[ "$line" =~ ^"#" ]]; then | |
| echo -e " ${DIM}${line}${RESET}" | |
| fi | |
| done | |
| rm -rf "$build_dir" | |
| if image_exists; then | |
| echo "" | |
| success "Image built successfully" | |
| else | |
| echo "" | |
| error "Image build failed" | |
| exit 1 | |
| fi | |
| } | |
| echo "" | |
| if $FORCE_REBUILD; then | |
| step "Full rebuild" | |
| # Nuke everything | |
| if docker volume inspect "$HOME_VOLUME" &>/dev/null; then | |
| docker volume rm "$HOME_VOLUME" >/dev/null 2>&1 || true | |
| warn "Removed home volume" | |
| fi | |
| if image_exists; then | |
| docker rmi "${IMAGE_NAME}:${IMAGE_TAG}" >/dev/null 2>&1 || true | |
| warn "Removed old image" | |
| fi | |
| build_image | |
| elif ! image_exists; then | |
| info "Image not found — building for the first time" | |
| build_image | |
| else | |
| success "Image ${BOLD}${IMAGE_NAME}:${IMAGE_TAG}${RESET} ready" | |
| fi | |
| # ── Handle home volume ─────────────────────────────────────────────────────── | |
| echo "" | |
| step "Home volume" | |
| FIRST_RUN=false | |
| if ! docker volume inspect "$HOME_VOLUME" &>/dev/null; then | |
| docker volume create "$HOME_VOLUME" >/dev/null | |
| info "Created ${BOLD}${HOME_VOLUME}${RESET}" | |
| FIRST_RUN=true | |
| else | |
| success "Volume ready ${DIM}(claude, mise cached)${RESET}" | |
| fi | |
| if $FIRST_RUN; then | |
| warn "First run — tools will install on startup (~30s)" | |
| warn "You'll need to authenticate Claude Code once" | |
| fi | |
| # ── Gather Mission ────────────────────────────────────────────────────────── | |
| MISSION="" | |
| if ! $SHELL_MODE && [[ -z "$RESUME_ID" ]]; then | |
| echo "" | |
| step "Mission briefing" | |
| echo "" | |
| echo -e " ${DIM}What should Claude work on? Describe the task, goal, or${RESET}" | |
| echo -e " ${DIM}feature you want built. Be as specific as you like.${RESET}" | |
| echo -e " ${DIM}(Empty = interactive mode, no initial prompt)${RESET}" | |
| echo "" | |
| echo -e " ${BOLD}${WHITE}Mission:${RESET}" | |
| echo -ne " ${CYAN}▸${RESET} " | |
| IFS= read -r MISSION | |
| if [[ -n "$MISSION" ]]; then | |
| echo "" | |
| echo -e " ${DIM}Any constraints, preferences, or context? (optional)${RESET}" | |
| echo -ne " ${CYAN}▸${RESET} " | |
| IFS= read -r CONTEXT | |
| if [[ -n "$CONTEXT" ]]; then | |
| MISSION="${MISSION} | |
| Additional context: ${CONTEXT}" | |
| fi | |
| fi | |
| fi | |
| # ── Launch Container ───────────────────────────────────────────────────────── | |
| echo "" | |
| step "Launching" | |
| CONTAINER_NAME="megaclaude-${PROJECT_NAME}-$$" | |
| EXTRA_ARGS="" | |
| if $SHELL_MODE; then | |
| EXTRA_ARGS="--shell" | |
| info "Mode: ${YELLOW}shell${RESET}" | |
| elif [[ -n "$RESUME_ID" ]]; then | |
| info "Mode: ${MAGENTA}claude --dangerously-skip-permissions --resume${RESET}" | |
| info "Resume: ${CYAN}${RESUME_ID}${RESET}" | |
| elif [[ -n "$MISSION" ]]; then | |
| info "Mode: ${MAGENTA}claude --dangerously-skip-permissions${RESET} ${DIM}(mission loaded)${RESET}" | |
| else | |
| info "Mode: ${MAGENTA}claude --dangerously-skip-permissions${RESET}" | |
| fi | |
| info "Project: ${BOLD}${PROJECT_DIR}${RESET} ${DIM}→ /workspace${RESET}" | |
| echo "" | |
| echo -e " ${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" | |
| echo "" | |
| docker run -it --rm \ | |
| --name "$CONTAINER_NAME" \ | |
| -v "${PROJECT_DIR}:/workspace" \ | |
| -v "${HOME_VOLUME}:/home/${HOST_USER}" \ | |
| -e "DEV_UID=${HOST_UID}" \ | |
| -e "DEV_GID=${HOST_GID}" \ | |
| -e "DEV_USER=${HOST_USER}" \ | |
| -e MEGA_MISSION="$MISSION" \ | |
| -e "MEGA_RESUME=${RESUME_ID}" \ | |
| -e "TERM=${TERM:-xterm-256color}" \ | |
| -e "LANG=en_US.UTF-8" \ | |
| --hostname "megaclaude" \ | |
| "${IMAGE_NAME}:${IMAGE_TAG}" \ | |
| ${EXTRA_ARGS:+"$EXTRA_ARGS"} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment