Skip to content

claude-code: add template .claude/ config and improve adoption docs #59

Description

@gatezh

Context

Comparing the claude-code/ template against a consumer project (utili) that uses these images. The README is thorough, but the template files themselves have gaps that make adoption harder than it should be.

Missing template files

1. No .claude/settings.json template

Consumer projects need a starting settings.json with common permissions. Without one, every adopter reinvents this from scratch:

{
  "permissions": {
    "allow": [
      "Bash(bun:*)",
      "Bash(wrangler:*)",
      "Bash(git:*)",
      "Bash(curl:*)",
      "Bash(mkdir:*)",
      "Bash(ls:*)"
    ]
  }
}

Should be in claude-code/.claude/settings.json as a starting point.

2. init-plugins.sh not in claude-code/ directory

The README references it as optional and links to the repo's own .devcontainer/init-plugins.sh, but adopters have to navigate to a different directory to find the template. Should be alongside the other template files in claude-code/.devcontainer/init-plugins.sh.

3. No Codex CLI integration

Some consumers use both Claude Code and Codex. This project adds Codex support via:

"postStartCommand": "ln -sfn /workspace/.codex ~/.codex"

Worth documenting as an optional pattern in the README or including in the template with a comment.

Template improvements

4. Node_modules mounts are too opinionated

The template devcontainer.json hardcodes services/api, services/app, services/www, packages/shared, packages/database paths. Projects with different structures have to understand and rewrite the entire mounts section.

Consider either:

  • A minimal template with just root node_modules + a comment explaining how to add more
  • Or a clearly marked "customize this section" block

5. Missing optional extension suggestions

The template only includes core extensions. Consumer projects commonly add these — worth listing in the README as recommended additions:

Extension Purpose
streetsidesoftware.code-spell-checker Catch typos
christian-kohler.npm-intellisense Autocomplete npm imports
mattpocock.ts-error-translator Human-readable TS errors
bierner.color-info + kamikillerto.vscode-colorize Inline CSS color previews
drizzle-team.drizzle-orm-snippets Drizzle ORM (if using Drizzle)
ms-playwright.playwright Playwright test runner

6. Default variant in consumer projects missing theme

The template correctly includes Claude Dark theme, but the consumer project's default variant was missing it (only sandbox had it). This suggests the theme was added to the template after the consumer project was set up. Worth noting in the README that existing consumers should sync theme settings:

"workbench.colorTheme": "Claude Dark",
"workbench.colorCustomizations": {
  "statusBarItem.remoteBackground": "#C15F3C",
  "statusBarItem.remoteForeground": "#ffffff"
}

7. Default and sandbox variants should share Docker volumes

The template uses separate volume prefixes — myproject-* for default and sandbox-* for sandbox. This means node_modules, Claude config, and fish history are duplicated across variants. Installing or updating packages in the default container has no effect in the sandbox (and vice versa), so they drift out of sync.

Both variants serve the same workspace. The sandbox's isolation is network-level (firewall), not package-level. Duplicating volumes adds no security benefit — it just means running bun install twice and wasting disk space.

The template should use the same volume prefix for both variants so they share state:

- "source=sandbox-node-modules-root-${devcontainerId},target=/workspace/node_modules,type=volume",
+ "source=myproject-node-modules-root-${devcontainerId},target=/workspace/node_modules,type=volume",

8. Compose project name collides across consumer projects

The template's docker-compose.yml files don't set a name: field, so Docker Compose derives the project name from the parent directory — claude-sandbox for the sandbox variant. Every consumer project using this template gets the same project name, causing container collisions when switching between projects.

What happens: Opening project A creates container claude-sandbox-devcontainer-1 with bind mounts pointing to project A's paths. Opening project B finds the existing container (same project name), tries to reuse it with --no-recreate, and fails because the bind mounts still point to project A:

error mounting "/host_mnt/Users/.../projectA/.devcontainer/claude-sandbox/init-firewall.sh"
  to rootfs at "/usr/local/bin/init-firewall.sh": not a directory

Fix: The template should document that consumers must set a unique name: in their docker-compose.yml:

name: myproject-sandbox  # <-- unique per consumer project
services:
  devcontainer:
    image: ghcr.io/gatezh/devcontainer-images/claude-code-sandbox:latest
    pull_policy: always
    volumes:
      - ../..:/workspace:cached

The Dev Containers CLI reads name from docker compose config output and passes it as --project-name in subsequent commands, so this works without any CLI changes.

Priority

Items 1-2 (missing template files) are the highest friction for new adopters. Items 3-6 are nice-to-haves. Items 7-8 are design issues that cause practical problems for anyone using both variants or multiple consumer projects.

Activity

  1. gatezh commented on Mar 28, 2026

    @gatezh
    OwnerAuthor

    Addressed in #60:

    # Point Status
    1 .claude/settings.json template ✅ Added with common permission allowlists
    2 init-plugins.sh location ✅ Already fixed in #58
    3 Codex CLI integration Skipped for now
    4 Simplified node_modules mounts ✅ Root-only + commented monorepo examples
    5 Recommended extensions ✅ Added to README
    6 Default variant theme ✅ Already fixed in #58
    7 Shared volumes between variants ✅ Switched to ${localWorkspaceFolderBasename} — both variants share volumes while supporting simultaneous use
    8 Compose project name collision ✅ Added name: field to both compose files

    Note on #7: The original suggestion to just align the sandbox-* → myproject-* prefix wouldn't actually share volumes because ${devcontainerId} is a hash that includes the devcontainer.json file path — different configs produce different hashes. Switched to ${localWorkspaceFolderBasename} which resolves to the same value for both variants since they open from the same project root.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions