diff --git a/.planning/STATE.md b/.planning/STATE.md index 71da506..d672545 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -11,7 +11,7 @@ See: .planning/PROJECT.md (updated 2026-02-24) Milestone: v1.0 MVP — SHIPPED 2026-02-24 Status: Milestone Complete -Last activity: 2026-03-23 - Completed quick task 260322-wyy: add gstack to CLAUDE, update README, validate it works by building and creating a mock devcontainer +Last activity: 2026-03-29 - Completed quick task 260329-lnd: create entrypoint merge mechanism for ~/.claude host bind-mounts Progress: [##########] 100% @@ -37,9 +37,10 @@ None. |---|-------------|------|--------|-----------| | 1 | Add tmux theme and ease of use config to base Dockerfile | 2026-03-08 | 7fc04c9 | [1-add-tmux-theme-and-ease-of-use-config-to](./quick/1-add-tmux-theme-and-ease-of-use-config-to/) | | 260322-wyy | add gstack to CLAUDE, update README, validate it works by building and creating a mock devcontainer | 2026-03-23 | 9ef5374 | [260322-wyy-add-gstack-to-claude-update-readme-valid](./quick/260322-wyy-add-gstack-to-claude-update-readme-valid/) | +| 260329-lnd | create entrypoint merge mechanism for ~/.claude host bind-mounts | 2026-03-29 | 4c86838 | [260329-lnd-create-entrypoint-merge-mechanism-for-cl](./quick/260329-lnd-create-entrypoint-merge-mechanism-for-cl/) | ## Session Continuity -Last session: 2026-03-23 -Stopped at: Completed quick task 260322-wyy (gstack + Bun) +Last session: 2026-03-29 +Stopped at: Completed quick task 260329-lnd (entrypoint merge mechanism) Resume file: None diff --git a/.planning/quick/260329-lnd-create-entrypoint-merge-mechanism-for-cl/260329-lnd-PLAN.md b/.planning/quick/260329-lnd-create-entrypoint-merge-mechanism-for-cl/260329-lnd-PLAN.md new file mode 100644 index 0000000..2f95220 --- /dev/null +++ b/.planning/quick/260329-lnd-create-entrypoint-merge-mechanism-for-cl/260329-lnd-PLAN.md @@ -0,0 +1,241 @@ +--- +phase: quick +plan: 260329-lnd +type: execute +wave: 1 +depends_on: [] +files_modified: + - scripts/merge-claude-home.sh + - scripts/docker-sock-fix.sh + - base/Dockerfile + - base/devcontainer-claude.md +autonomous: true +requirements: [QUICK-260329-LND] + +must_haves: + truths: + - "Full ~/.claude dir mount results in image tooling (plugins, skills, rules, GSD) present alongside host auth files" + - "Individual file mounts (credentials.json, settings.json only) work without merge logic triggering" + - "Host CLAUDE.md content is preserved and image CLAUDE.md content is appended" + - "Merge is idempotent — running the entrypoint multiple times (container restart) does not duplicate content" + - "Existing docker-sock-fix.sh behavior is unchanged" + artifacts: + - path: "scripts/merge-claude-home.sh" + provides: "Merge logic for ~/.claude host mount" + - path: "scripts/docker-sock-fix.sh" + provides: "Entrypoint that calls merge script before exec" + - path: "base/Dockerfile" + provides: "Build-time snapshot of ~/.claude to /opt/devcontainer-claude" + key_links: + - from: "base/Dockerfile" + to: "/opt/devcontainer-claude/" + via: "cp -a at build time after setup-claude.sh" + pattern: "cp -a.*\\.claude.*devcontainer-claude" + - from: "scripts/docker-sock-fix.sh" + to: "scripts/merge-claude-home.sh" + via: "source or call before exec" + pattern: "merge-claude-home" + - from: "scripts/merge-claude-home.sh" + to: "/opt/devcontainer-claude/" + via: "rsync/cp from backup into live ~/.claude" + pattern: "devcontainer-claude" +--- + + +Create an entrypoint merge mechanism so that when a developer bind-mounts their host ~/.claude directory (or individual files from it) into the devcontainer, image-built tooling (plugins, skills, rules, GSD, CLAUDE.md) is merged in without overwriting host auth/settings files. + +Purpose: Developers need host credentials (credentials.json, settings.json) in the container, but mounting ~/.claude shadows all build-time tooling. This merge script restores image tooling additively at container start. + +Output: New merge script, updated entrypoint, updated Dockerfile with build-time snapshot. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@scripts/docker-sock-fix.sh +@scripts/setup-claude.sh +@base/Dockerfile +@base/devcontainer-claude.md +@scripts/init-claude-mcp.sh + + + + + + Task 1: Create merge-claude-home.sh and snapshot in Dockerfile + scripts/merge-claude-home.sh, base/Dockerfile + +**Create `scripts/merge-claude-home.sh`** — a POSIX-compatible bash script that: + +1. **Detect mount**: Check if `~/.claude` was volume-mounted by comparing device IDs: + ``` + BACKUP="/opt/devcontainer-claude" + LIVE="$HOME/.claude" + # If backup doesn't exist, nothing to merge (image not built with snapshot) + [ -d "$BACKUP" ] || exit 0 + # Use a marker file approach: at build time we create $BACKUP/.image-marker + # If $LIVE/.image-marker exists and matches, no mount happened — skip + if [ -f "$LIVE/.image-marker" ] && [ "$(cat "$LIVE/.image-marker")" = "$(cat "$BACKUP/.image-marker")" ]; then + exit 0 + fi + ``` + +2. **Idempotency marker**: Create `$LIVE/.merge-done` with a hash of the backup marker. If this file exists and matches, skip merge (handles container restart without re-merging). + +3. **Merge directories** — for each of these subdirectories, use `cp -rn` (no-clobber) from backup to live: + - `plugins/` + - `skills/` + - `rules/` + - `get-shit-done/` + - `agents/` (if exists in backup) + + The `-n` flag ensures host files are never overwritten. Only missing files/dirs are filled in. + +4. **Merge CLAUDE.md** — special handling: + - If `$LIVE/CLAUDE.md` does not exist, copy from backup + - If `$LIVE/CLAUDE.md` exists but does NOT contain the marker `# DevContainer Environment` (the heading from devcontainer-claude.md), append the backup CLAUDE.md content with a separator: + ``` + printf '\n\n# --- DevContainer Image (auto-merged) ---\n\n' >> "$LIVE/CLAUDE.md" + cat "$BACKUP/CLAUDE.md" >> "$LIVE/CLAUDE.md" + ``` + - If it already contains `# DevContainer Environment`, skip (already merged or image-native) + +5. **Preserve host files**: Never touch `credentials.json`, `settings.json`, `projects/`, `.claude.json`, or any file that already exists in the live directory (cp -n handles this). + +6. Write the idempotency marker at the end. + +7. The script must be `set -e`, use POSIX-compatible redirects (`>/dev/null 2>&1`), and exit 0 on all paths (entrypoint must not fail). + +**Update `base/Dockerfile`** — add two steps after the `setup-claude.sh` RUN (line ~234): + +1. Create the image marker: + ```dockerfile + RUN echo "devcontainer-$(date +%s)" > $HOME/.claude/.image-marker + ``` + +2. Snapshot the entire `~/.claude/` to `/opt/devcontainer-claude/`: + ```dockerfile + USER root + RUN cp -a /home/dev/.claude /opt/devcontainer-claude \ + && chown -R dev:dev /opt/devcontainer-claude + USER dev + ``` + Place this BEFORE the `USER root` final section (before line ~240). Since the Dockerfile already switches to `USER root` at line ~240, fold the snapshot into that section to avoid extra USER switches. Specifically: + - Insert `RUN cp -a /home/dev/.claude /opt/devcontainer-claude` right after switching to USER root in the final section (after line 243 area) + +3. Copy the merge script into the image: + ```dockerfile + COPY --chown=dev:dev scripts/merge-claude-home.sh $HOME/.local/bin/merge-claude-home.sh + ``` + Place this near the other script COPY lines (around line 228-231). Ensure chmod +x includes it. + +Important: The image marker RUN must come AFTER `setup-claude.sh` runs but BEFORE the snapshot copy. The snapshot must capture the full post-setup state. + + + bash -n /workspace/scripts/merge-claude-home.sh && grep -q 'devcontainer-claude' /workspace/scripts/merge-claude-home.sh && grep -q 'merge-claude-home' /workspace/base/Dockerfile && grep -q 'opt/devcontainer-claude' /workspace/base/Dockerfile && echo "PASS" + + + - merge-claude-home.sh exists, passes bash syntax check, handles detect/merge/idempotency + - Dockerfile creates image marker, snapshots ~/.claude to /opt/devcontainer-claude, copies merge script + + + + + Task 2: Wire merge into entrypoint and update devcontainer-claude.md + scripts/docker-sock-fix.sh, base/devcontainer-claude.md + +**Update `scripts/docker-sock-fix.sh`** — call the merge script early in the entrypoint, BEFORE the docker socket logic: + +Insert after the shebang/comment block (after line 6), before the `SOCKET=` line: + +```bash +# Merge image-built ~/.claude tooling if host directory was mounted +if [ -x "$HOME/.local/bin/merge-claude-home.sh" ]; then + "$HOME/.local/bin/merge-claude-home.sh" || true +fi +``` + +The `|| true` ensures the entrypoint never fails even if merge has issues. Keep all existing docker socket logic unchanged. + +**Update `base/devcontainer-claude.md`** — add a short section documenting the merge mechanism under `## Notes`: + +Add after the existing notes bullet about shell: + +``` +- When host `~/.claude` is bind-mounted, the entrypoint auto-merges image tooling (plugins, skills, rules, GSD) into the mounted directory. Host files (credentials, settings) are never overwritten. +``` + +This keeps the in-image documentation accurate for developers. + + + grep -q 'merge-claude-home' /workspace/scripts/docker-sock-fix.sh && grep -q 'auto-merges' /workspace/base/devcontainer-claude.md && echo "PASS" + + + - docker-sock-fix.sh calls merge script before docker socket logic + - devcontainer-claude.md documents the merge behavior + - Existing entrypoint behavior (docker socket fix, exec "$@") is preserved + + + + + Task 3: Validate with Docker build dry-run + + +Run a Docker build of the base image to verify the Dockerfile changes are syntactically valid and the snapshot step succeeds. Use: + +```bash +docker build -f base/Dockerfile -t devcontainer-base:merge-test --target= . 2>&1 | tail -50 +``` + +If the full build is too slow or fails due to network (build args, Claude CLI download), at minimum validate: +1. `docker build --check` or parse the Dockerfile for syntax +2. Run `bash -n scripts/merge-claude-home.sh` (syntax check) +3. Run `bash -n scripts/docker-sock-fix.sh` (syntax check) +4. Verify the merge script handles all three scenarios by tracing logic: + - No backup dir exists -> exits 0 + - Backup exists, marker matches -> exits 0 (no mount) + - Backup exists, marker differs -> runs merge + +If Docker build succeeds, verify `/opt/devcontainer-claude` exists in the image: +```bash +docker run --rm devcontainer-base:merge-test ls /opt/devcontainer-claude/ +``` + +Note: A full Docker build may take a long time. If it exceeds 5 minutes, skip and rely on syntax validation + manual CI check on PR. + + + bash -n /workspace/scripts/merge-claude-home.sh && bash -n /workspace/scripts/docker-sock-fix.sh && echo "PASS" + + + - Both scripts pass bash syntax validation + - Dockerfile is buildable (or syntax-valid if full build skipped) + - Merge script logic covers: no-op when no mount, idempotent on restart, additive merge on mount + + + + + + +1. `bash -n scripts/merge-claude-home.sh` — syntax valid +2. `bash -n scripts/docker-sock-fix.sh` — syntax valid +3. `grep -c 'devcontainer-claude' base/Dockerfile` — returns 2+ (snapshot + copy) +4. `grep -c 'merge-claude-home' scripts/docker-sock-fix.sh` — returns 1+ +5. Dockerfile builds successfully (or passes syntax check) + + + +- New merge-claude-home.sh script handles three scenarios: no mount, individual file mount, full directory mount +- Merge is additive (cp -n) and idempotent (marker file) +- CLAUDE.md gets special append-merge treatment +- Entrypoint calls merge before existing logic +- Dockerfile snapshots build-time ~/.claude to /opt/devcontainer-claude +- All scripts pass bash syntax validation +- devcontainer-claude.md documents the merge behavior + + + +After completion, create `.planning/quick/260329-lnd-create-entrypoint-merge-mechanism-for-cl/260329-lnd-SUMMARY.md` + diff --git a/.planning/quick/260329-lnd-create-entrypoint-merge-mechanism-for-cl/260329-lnd-SUMMARY.md b/.planning/quick/260329-lnd-create-entrypoint-merge-mechanism-for-cl/260329-lnd-SUMMARY.md new file mode 100644 index 0000000..d87d138 --- /dev/null +++ b/.planning/quick/260329-lnd-create-entrypoint-merge-mechanism-for-cl/260329-lnd-SUMMARY.md @@ -0,0 +1,70 @@ +--- +phase: quick +plan: 260329-lnd +subsystem: devcontainer-base +tags: [entrypoint, merge, claude-tooling, bind-mount] +dependency_graph: + requires: [setup-claude.sh, docker-sock-fix.sh] + provides: [merge-claude-home.sh, /opt/devcontainer-claude snapshot] + affects: [base/Dockerfile, docker-sock-fix.sh, devcontainer-claude.md] +tech_stack: + added: [] + patterns: [build-time-snapshot, entrypoint-merge, idempotent-marker] +key_files: + created: + - scripts/merge-claude-home.sh + modified: + - base/Dockerfile + - scripts/docker-sock-fix.sh + - base/devcontainer-claude.md +decisions: + - Used cp -rn (no-clobber) for directory merge to avoid overwriting host files + - Used image marker file comparison for mount detection instead of device ID comparison + - Snapshot placed in final root section to avoid extra USER switches +metrics: + duration: 2m + completed: 2026-03-29 +--- + +# Quick Task 260329-lnd: Create Entrypoint Merge Mechanism for ~/.claude Summary + +POSIX-compatible entrypoint merge script that detects host ~/.claude bind-mounts and additively restores image-built tooling (plugins, skills, rules, GSD, CLAUDE.md) without overwriting host auth/settings files, using build-time snapshot and idempotent marker files. + +## What Was Done + +### Task 1: Create merge-claude-home.sh and snapshot in Dockerfile +- Created `scripts/merge-claude-home.sh` with three-scenario detection: no backup, no mount, host mount +- Mount detection via image marker file comparison +- Idempotent merge via `.merge-done` marker (handles container restarts) +- Directory merge uses `cp -rn` (no-clobber) for plugins, skills, rules, get-shit-done, agents +- CLAUDE.md gets special append-merge: host content preserved, image content appended with separator +- Updated Dockerfile: image marker after setup-claude.sh, snapshot to `/opt/devcontainer-claude` in root section, merge script COPY with chmod +x +- Commit: 700cdfe + +### Task 2: Wire merge into entrypoint and update devcontainer-claude.md +- Added merge-claude-home.sh call early in docker-sock-fix.sh (before socket logic) +- Protected with `|| true` so entrypoint never fails on merge issues +- Documented merge behavior in devcontainer-claude.md Notes section +- Commit: a22725d + +### Task 3: Validate with Docker build dry-run +- Both scripts pass `bash -n` syntax validation +- Dockerfile passes instruction-level validation +- Merge script logic traced through all three scenarios +- Dockerfile contains 4 references to devcontainer-claude (snapshot, copy, marker) +- Entrypoint contains 2 references to merge-claude-home (comment + call) + +## Deviations from Plan + +None - plan executed exactly as written. + +## Known Stubs + +None. + +## Commits + +| # | Hash | Message | +|---|------|---------| +| 1 | 700cdfe | feat(quick-260329-lnd): create merge-claude-home.sh and Dockerfile snapshot | +| 2 | a22725d | feat(quick-260329-lnd): wire merge into entrypoint and document in devcontainer-claude.md | diff --git a/base/Dockerfile b/base/Dockerfile index 4669622..1bae253 100644 --- a/base/Dockerfile +++ b/base/Dockerfile @@ -228,11 +228,15 @@ ENV NTFY_TOKEN=${NTFY_TOKEN} COPY --chown=dev:dev scripts/ntfy-hook.sh $HOME/.local/bin/ntfy-hook.sh COPY --chown=dev:dev scripts/suggest-context7-hook.sh $HOME/.local/bin/suggest-context7-hook.sh COPY --chown=dev:dev scripts/init-claude-mcp.sh $HOME/.local/bin/init-claude-mcp.sh -RUN chmod +x $HOME/.local/bin/ntfy-hook.sh $HOME/.local/bin/suggest-context7-hook.sh $HOME/.local/bin/init-claude-mcp.sh +COPY --chown=dev:dev scripts/merge-claude-home.sh $HOME/.local/bin/merge-claude-home.sh +RUN chmod +x $HOME/.local/bin/ntfy-hook.sh $HOME/.local/bin/suggest-context7-hook.sh $HOME/.local/bin/init-claude-mcp.sh $HOME/.local/bin/merge-claude-home.sh COPY --chown=dev:dev scripts/setup-claude.sh /tmp/setup-claude.sh RUN bash /tmp/setup-claude.sh && rm -f /tmp/setup-claude.sh +# Create image marker for mount detection (must be AFTER setup-claude.sh) +RUN echo "devcontainer-$(date +%s)" > $HOME/.claude/.image-marker + RUN echo 'source $HOME/.local/bin/init-claude-mcp.sh 2>/dev/null' >> /home/dev/.bashrc RUN tldr --update @@ -241,6 +245,10 @@ RUN tldr --update USER root +# Snapshot ~/.claude to /opt/devcontainer-claude (for merge on host mount) +RUN cp -a /home/dev/.claude /opt/devcontainer-claude \ + && chown -R dev:dev /opt/devcontainer-claude + RUN adduser dev sudo RUN passwd -d dev diff --git a/base/devcontainer-claude.md b/base/devcontainer-claude.md index e7815e0..87fd4ac 100644 --- a/base/devcontainer-claude.md +++ b/base/devcontainer-claude.md @@ -78,3 +78,4 @@ Core workflow: ## Notes - Shell is bash. `/bin/sh` is symlinked to `/bin/bash`. - Passwordless sudo is available via `sudo`. +- When host `~/.claude` is bind-mounted, the entrypoint auto-merges image tooling (plugins, skills, rules, GSD) into the mounted directory. Host files (credentials, settings) are never overwritten. diff --git a/scripts/docker-sock-fix.sh b/scripts/docker-sock-fix.sh index 8e98d52..d4c0643 100644 --- a/scripts/docker-sock-fix.sh +++ b/scripts/docker-sock-fix.sh @@ -4,6 +4,11 @@ # match the container's "docker" group. This script updates the container's # docker group GID to match the socket's GID, then re-execs with the new group. +# Merge image-built ~/.claude tooling if host directory was mounted +if [ -x "$HOME/.local/bin/merge-claude-home.sh" ]; then + "$HOME/.local/bin/merge-claude-home.sh" || true +fi + SOCKET="/var/run/docker.sock" if [ -S "$SOCKET" ]; then diff --git a/scripts/merge-claude-home.sh b/scripts/merge-claude-home.sh new file mode 100644 index 0000000..1b6bf16 --- /dev/null +++ b/scripts/merge-claude-home.sh @@ -0,0 +1,67 @@ +#!/bin/bash +set -e + +# Merge image-built ~/.claude tooling into a host-mounted ~/.claude directory. +# Called by the entrypoint (docker-sock-fix.sh) at container start. +# +# Three scenarios: +# 1. No backup exists (/opt/devcontainer-claude missing) -> exit 0 +# 2. No mount detected (image marker matches) -> exit 0 +# 3. Host mount detected -> merge image tooling additively, skip host files + +BACKUP="/opt/devcontainer-claude" +LIVE="$HOME/.claude" + +# If backup doesn't exist, nothing to merge (image not built with snapshot) +if [ ! -d "$BACKUP" ]; then + exit 0 +fi + +# If the live directory has the same image marker as the backup, no mount happened +if [ -f "$LIVE/.image-marker" ] && [ -f "$BACKUP/.image-marker" ]; then + if [ "$(cat "$LIVE/.image-marker")" = "$(cat "$BACKUP/.image-marker")" ]; then + exit 0 + fi +fi + +# Idempotency: if we already merged for this backup version, skip +BACKUP_HASH="" +if [ -f "$BACKUP/.image-marker" ]; then + BACKUP_HASH=$(cat "$BACKUP/.image-marker") +fi + +if [ -f "$LIVE/.merge-done" ] && [ "$(cat "$LIVE/.merge-done")" = "$BACKUP_HASH" ]; then + exit 0 +fi + +# Ensure live directory exists +mkdir -p "$LIVE" + +# Merge directories — cp -rn (no-clobber) so host files are never overwritten +for dir in plugins skills rules get-shit-done agents; do + if [ -d "$BACKUP/$dir" ]; then + if [ ! -d "$LIVE/$dir" ]; then + cp -r "$BACKUP/$dir" "$LIVE/$dir" + else + cp -rn "$BACKUP/$dir/." "$LIVE/$dir/" 2>/dev/null || true + fi + fi +done + +# Merge CLAUDE.md — special append logic +if [ -f "$BACKUP/CLAUDE.md" ]; then + if [ ! -f "$LIVE/CLAUDE.md" ]; then + # No host CLAUDE.md — copy from backup + cp "$BACKUP/CLAUDE.md" "$LIVE/CLAUDE.md" + elif ! grep -q '# DevContainer Environment' "$LIVE/CLAUDE.md" 2>/dev/null; then + # Host CLAUDE.md exists but lacks DevContainer section — append + printf '\n\n# --- DevContainer Image (auto-merged) ---\n\n' >>"$LIVE/CLAUDE.md" + cat "$BACKUP/CLAUDE.md" >>"$LIVE/CLAUDE.md" + fi + # If already contains '# DevContainer Environment', skip (already merged or image-native) +fi + +# Write idempotency marker +echo "$BACKUP_HASH" >"$LIVE/.merge-done" + +exit 0