sandbox-vibe 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts","../src/config.ts","../src/log.ts","../src/paths.ts","../src/render.ts","../src/commands/bumpMarker.ts","../src/commands/init.ts","../src/prompts.ts","../src/sensitive-paths.ts","../src/docker.ts","../src/commands/up.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { Command } from \"commander\";\nimport { readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { bumpMarker } from \"./commands/bumpMarker.js\";\nimport { init } from \"./commands/init.js\";\nimport { up } from \"./commands/up.js\";\nimport { log, logError } from \"./log.js\";\nimport { isAbortError } from \"./prompts.js\";\n\nconst PKG_PATH = join(\n dirname(fileURLToPath(import.meta.url)),\n \"..\",\n \"package.json\",\n);\n\nfunction getVersion(): string {\n try {\n const pkg = JSON.parse(readFileSync(PKG_PATH, \"utf-8\")) as {\n version: string;\n };\n return pkg.version;\n } catch {\n return \"0.0.0\";\n }\n}\n\nconst program = new Command();\n\nprogram\n .name(\"sandbox-vibe\")\n .description(\n \"Plug-and-play Docker sandbox for Claude Code with idempotent plugin and MCP bootstrap and security limits enforced by default.\",\n )\n .version(getVersion(), \"-v, --version\", \"output the current version\");\n\nprogram\n .command(\"init\")\n .description(\n \"Generate the .sandbox-vibe/ directory in the current project.\",\n )\n .option(\n \"-f, --force\",\n \"overwrite an existing .sandbox-vibe/ without confirmation\",\n )\n .option(\n \"--non-interactive\",\n \"skip the wizard and use defaults (suitable for CI)\",\n )\n .action(\n async (opts: { force?: boolean; nonInteractive?: boolean }) => {\n await init({\n force: opts.force,\n nonInteractive: opts.nonInteractive,\n });\n },\n );\n\nprogram\n .command(\"up\")\n .description(\"Build the sandbox images and drop into the Claude REPL.\")\n .action(async () => {\n await up();\n });\n\nprogram\n .command(\"bump-marker\")\n .description(\n \"Increment the bootstrap marker to force re-bootstrap on next up.\",\n )\n .action(async () => {\n await bumpMarker();\n });\n\ntry {\n await program.parseAsync(process.argv);\n} catch (err) {\n if (isAbortError(err)) {\n log(\"Aborted.\");\n process.exit(0);\n }\n logError(describeError(err));\n process.exit(1);\n}\n\nfunction describeError(err: unknown): string {\n let raw: string;\n if (err instanceof Error) {\n // ExecaError exposes `shortMessage` (command + reason, without stdout/\n // stderr blocks). Prefer it; full `message` can be hundreds of lines.\n const maybeExeca = err as { shortMessage?: unknown };\n raw =\n typeof maybeExeca.shortMessage === \"string\"\n ? maybeExeca.shortMessage\n : err.message;\n } else {\n raw = `Unknown error: ${String(err)}`;\n }\n // Replace the user's home directory with `~` so error output dumped\n // into shared logs / CI does not leak the absolute filesystem layout.\n const home = homedir();\n return home.length > 0 ? raw.replaceAll(home, \"~\") : raw;\n}\n","import { readFileSync, unlinkSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nexport type Stack = \"none\" | \"php\" | \"dotnet\" | \"python\" | \"go\" | \"rust\";\n\nexport type AdditionalMount = {\n hostPath: string;\n containerPath: string;\n readonly: boolean;\n};\n\nexport type Resources = {\n cpus: number;\n memoryGB: number;\n pids: number;\n tmpfsMB: number;\n};\n\nexport type Mcp = {\n name: string;\n transport: \"http\" | \"sse\";\n url: string;\n};\n\nexport type Config = {\n schemaVersion: 1;\n workspacePath: string;\n additionalMounts: AdditionalMount[];\n resources: Resources;\n stack: Stack;\n plugins: string[];\n marketplaces: string[];\n mcps: Mcp[];\n marker: string;\n};\n\nexport const CONFIG_FILE_NAME = \"config.json\";\n\nexport const STACKS: readonly Stack[] = [\n \"none\",\n \"php\",\n \"dotnet\",\n \"python\",\n \"go\",\n \"rust\",\n];\n\nexport const DEFAULT_PLUGINS: string[] = [\n \"security-guidance@claude-plugins-official\",\n \"commit-commands@claude-plugins-official\",\n \"code-review@claude-plugins-official\",\n \"pr-review-toolkit@claude-plugins-official\",\n \"claude-md-management@claude-plugins-official\",\n \"hookify@claude-plugins-official\",\n \"feature-dev@claude-plugins-official\",\n \"superpowers@superpowers-dev\",\n];\n\nexport const DEFAULT_MARKETPLACES: string[] = [\n \"anthropics/claude-plugins-official\",\n \"obra/superpowers\",\n];\n\nexport const DEFAULT_RESOURCES: Resources = {\n cpus: 4,\n memoryGB: 4,\n pids: 256,\n tmpfsMB: 512,\n};\n\n// Strict identifier patterns. Defense in depth against shell injection\n// when these strings are interpolated into the bash entrypoint of the\n// generated docker-compose.override.yml. The character classes below were\n// chosen to cover legitimate plugin / marketplace / MCP names while\n// rejecting every shell metacharacter (`;`, `&`, `|`, `$`, backtick,\n// quotes, newline, space).\nexport const PLUGIN_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*@[A-Za-z0-9][A-Za-z0-9._/-]*$/;\nexport const MARKETPLACE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*\\/[A-Za-z0-9][A-Za-z0-9._-]*$/;\nexport const MCP_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;\n\nexport function validatePluginRef(value: string): string | true {\n if (!PLUGIN_PATTERN.test(value)) {\n return \"must match name@marketplace using letters, digits, '.', '_', '-' (no spaces or shell metacharacters)\";\n }\n return true;\n}\n\nexport function validateMarketplaceRef(value: string): string | true {\n if (!MARKETPLACE_PATTERN.test(value)) {\n return \"must match owner/repo using letters, digits, '.', '_', '-' (no spaces or shell metacharacters)\";\n }\n return true;\n}\n\nexport function validateMcpName(value: string): string | true {\n if (!MCP_NAME_PATTERN.test(value)) {\n return \"must contain only letters, digits, '_' and '-'\";\n }\n return true;\n}\n\nexport function validateMcpUrl(value: string): string | true {\n if (/[\\r\\n\\t]/.test(value)) {\n return \"URL must not contain newline, carriage return, or tab\";\n }\n let parsed: URL;\n try {\n parsed = new URL(value);\n } catch {\n return \"must be a valid URL\";\n }\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n return \"URL must use http:// or https://\";\n }\n // Basic-auth credentials in the URL would be passed verbatim to\n // `claude mcp add ... <url>` in the entrypoint, surfaced in `ps`,\n // captured into the bootstrap log, and visible to anyone who can\n // read process state. Force callers to use proper auth headers.\n if (parsed.username !== \"\" || parsed.password !== \"\") {\n return \"URL must not contain basic-auth credentials (user:pass@); use Authorization headers instead\";\n }\n return true;\n}\n\nexport function validateHostPath(value: string): string | true {\n if (/[\\r\\n]/.test(value)) {\n return \"path must not contain newline or carriage return\";\n }\n // POSIX paths can technically contain ':' but that breaks the\n // host:container separator in docker-compose volumes. Reject defensively.\n if (value.includes(\":\")) {\n return \"path must not contain ':'\";\n }\n if (!value.startsWith(\"/\")) {\n return \"path must be absolute (start with '/')\";\n }\n return true;\n}\n\nexport function validateContainerPath(value: string): string | true {\n if (/[\\r\\n]/.test(value)) {\n return \"path must not contain newline or carriage return\";\n }\n if (value.includes(\":\")) {\n return \"path must not contain ':'\";\n }\n if (!value.startsWith(\"/\")) {\n return \"container path must be absolute (start with '/')\";\n }\n return true;\n}\n\nexport function loadConfig(vibeDir: string): Config {\n const configPath = join(vibeDir, CONFIG_FILE_NAME);\n const raw = readFileSync(configPath, \"utf-8\");\n const parsed: unknown = JSON.parse(raw);\n validateConfig(parsed);\n return parsed;\n}\n\nexport function saveConfig(vibeDir: string, config: Config): void {\n const configPath = join(vibeDir, CONFIG_FILE_NAME);\n // unlink first so a pre-existing symlink at configPath does not redirect\n // the write into a host file. Ignore ENOENT.\n try {\n unlinkSync(configPath);\n } catch {\n // Path doesn't exist or is not removable — writeFileSync below will\n // surface the real error if it is a problem.\n }\n writeFileSync(\n configPath,\n JSON.stringify(config, null, 2) + \"\\n\",\n \"utf-8\",\n );\n}\n\nfunction validateConfig(value: unknown): asserts value is Config {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new Error(\"config.json: expected an object at the root.\");\n }\n const cfg = value as Record<string, unknown>;\n\n if (cfg.schemaVersion !== 1) {\n throw new Error(\n `config.json: unsupported schemaVersion ${String(cfg.schemaVersion)}; expected 1.`,\n );\n }\n\n if (typeof cfg.workspacePath !== \"string\" || cfg.workspacePath.length === 0) {\n throw new Error(\"config.json: workspacePath must be a non-empty string.\");\n }\n const wsCheck = validateHostPath(cfg.workspacePath);\n if (wsCheck !== true) {\n throw new Error(`config.json: workspacePath ${wsCheck}.`);\n }\n\n if (typeof cfg.marker !== \"string\" || cfg.marker.length === 0) {\n throw new Error(\"config.json: marker must be a non-empty string.\");\n }\n if (!/^bootstrap-[a-f0-9]{16}(-r\\d+)?$/.test(cfg.marker)) {\n throw new Error(\n \"config.json: marker must match pattern 'bootstrap-<16 hex chars>' or with optional '-r<N>' suffix.\",\n );\n }\n\n if (\n typeof cfg.stack !== \"string\" ||\n !(STACKS as readonly string[]).includes(cfg.stack)\n ) {\n throw new Error(\n `config.json: stack must be one of ${STACKS.join(\"/\")}; got ${String(cfg.stack)}.`,\n );\n }\n\n if (\n !Array.isArray(cfg.plugins) ||\n cfg.plugins.some((p) => typeof p !== \"string\")\n ) {\n throw new Error(\"config.json: plugins must be an array of strings.\");\n }\n for (const p of cfg.plugins) {\n const r = validatePluginRef(p as string);\n if (r !== true) {\n throw new Error(`config.json: plugin '${String(p)}' invalid — ${r}.`);\n }\n }\n\n if (\n !Array.isArray(cfg.marketplaces) ||\n cfg.marketplaces.some((m) => typeof m !== \"string\")\n ) {\n throw new Error(\"config.json: marketplaces must be an array of strings.\");\n }\n for (const m of cfg.marketplaces) {\n const r = validateMarketplaceRef(m as string);\n if (r !== true) {\n throw new Error(`config.json: marketplace '${String(m)}' invalid — ${r}.`);\n }\n }\n\n if (!Array.isArray(cfg.mcps) || !cfg.mcps.every(isMcp)) {\n throw new Error(\n \"config.json: mcps must be an array of { name: string, transport: 'http'|'sse', url: string }.\",\n );\n }\n for (const m of cfg.mcps) {\n const nameCheck = validateMcpName(m.name);\n if (nameCheck !== true) {\n throw new Error(`config.json: mcps[].name '${m.name}' invalid — ${nameCheck}.`);\n }\n const urlCheck = validateMcpUrl(m.url);\n if (urlCheck !== true) {\n throw new Error(`config.json: mcps[].url '${m.url}' invalid — ${urlCheck}.`);\n }\n }\n\n if (\n !Array.isArray(cfg.additionalMounts) ||\n !cfg.additionalMounts.every(isAdditionalMount)\n ) {\n throw new Error(\n \"config.json: additionalMounts must be an array of { hostPath: string, containerPath: string, readonly: boolean }.\",\n );\n }\n for (const m of cfg.additionalMounts) {\n const hostCheck = validateHostPath(m.hostPath);\n if (hostCheck !== true) {\n throw new Error(\n `config.json: additionalMounts[].hostPath '${m.hostPath}' invalid — ${hostCheck}.`,\n );\n }\n const containerCheck = validateContainerPath(m.containerPath);\n if (containerCheck !== true) {\n throw new Error(\n `config.json: additionalMounts[].containerPath '${m.containerPath}' invalid — ${containerCheck}.`,\n );\n }\n }\n\n if (!isResources(cfg.resources)) {\n throw new Error(\n \"config.json: resources must be { cpus, memoryGB, pids, tmpfsMB } with positive numbers (pids and tmpfsMB must be integers).\",\n );\n }\n}\n\nfunction isMcp(value: unknown): value is Mcp {\n if (typeof value !== \"object\" || value === null) return false;\n const obj = value as Record<string, unknown>;\n return (\n typeof obj.name === \"string\" &&\n typeof obj.url === \"string\" &&\n (obj.transport === \"http\" || obj.transport === \"sse\")\n );\n}\n\nfunction isAdditionalMount(value: unknown): value is AdditionalMount {\n if (typeof value !== \"object\" || value === null) return false;\n const obj = value as Record<string, unknown>;\n return (\n typeof obj.hostPath === \"string\" &&\n typeof obj.containerPath === \"string\" &&\n typeof obj.readonly === \"boolean\"\n );\n}\n\nfunction isResources(value: unknown): value is Resources {\n if (typeof value !== \"object\" || value === null) return false;\n const obj = value as Record<string, unknown>;\n return (\n typeof obj.cpus === \"number\" &&\n Number.isFinite(obj.cpus) &&\n obj.cpus > 0 &&\n typeof obj.memoryGB === \"number\" &&\n Number.isFinite(obj.memoryGB) &&\n obj.memoryGB > 0 &&\n typeof obj.pids === \"number\" &&\n Number.isInteger(obj.pids) &&\n obj.pids > 0 &&\n typeof obj.tmpfsMB === \"number\" &&\n Number.isInteger(obj.tmpfsMB) &&\n obj.tmpfsMB > 0\n );\n}\n","const PREFIX = \"[sandbox-vibe]\";\n\nexport function log(message: string): void {\n console.log(`${PREFIX} ${message}`);\n}\n\nexport function logError(message: string): void {\n console.error(`${PREFIX} ${message}`);\n}\n","import { statSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\n\nexport const SANDBOX_VIBE_DIR = \".sandbox-vibe\";\n\nexport function findSandboxVibeDir(cwd: string = process.cwd()): string | null {\n const candidate = resolve(cwd, SANDBOX_VIBE_DIR);\n try {\n if (statSync(candidate).isDirectory()) return candidate;\n } catch {\n // ENOENT or any other stat failure: treat as \"not found\".\n }\n return null;\n}\n\nexport function sandboxVibePath(cwd: string, ...parts: string[]): string {\n return join(cwd, SANDBOX_VIBE_DIR, ...parts);\n}\n","import { createHash } from \"node:crypto\";\nimport { readFile, unlink, writeFile } from \"node:fs/promises\";\nimport { basename, dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { Config, Stack } from \"./config.js\";\n\nconst TEMPLATES_DIR = join(\n dirname(fileURLToPath(import.meta.url)),\n \"templates\",\n);\n\nexport const TEMPLATE_FILES = {\n baseDockerfile: {\n template: \"Dockerfile.sandbox.tpl\",\n output: \"Dockerfile.sandbox\",\n },\n baseCompose: {\n template: \"docker-compose.sandbox.yml.tpl\",\n output: \"docker-compose.sandbox.yml\",\n },\n overrideDockerfile: {\n template: \"Dockerfile.sandbox.override.tpl\",\n output: \"Dockerfile.sandbox.override\",\n },\n overrideCompose: {\n template: \"docker-compose.override.yml.tpl\",\n output: \"docker-compose.override.yml\",\n },\n} as const;\n\nexport async function renderTemplate(\n templateName: string,\n vars: Record<string, string>,\n): Promise<string> {\n const tplPath = join(TEMPLATES_DIR, templateName);\n const tpl = await readFile(tplPath, \"utf-8\");\n const lookup = (key: string): string => getRequiredVar(vars, key, templateName);\n\n // Pass 1: whole-line comment markers `# vibe-render:NAME`. Used in\n // Dockerfile templates so the placeholder is a syntactically valid\n // comment and Dockerfile language servers do not flag it as an unknown\n // instruction. The marker line is replaced entirely by the variable\n // value (which itself can be multi-line).\n const afterCommentMarkers = tpl.replace(\n /^[ \\t]*# vibe-render:(\\w+)[ \\t]*$/gm,\n (_match: string, key: string) => lookup(key),\n );\n\n // Pass 2: inline `${name}` placeholders for scalar values that must sit\n // inside an expression (compose YAML keys, the bootstrap marker name).\n return afterCommentMarkers.replace(\n /\\$\\{(\\w+)\\}/g,\n (_match: string, key: string) => lookup(key),\n );\n}\n\nfunction getRequiredVar(\n vars: Record<string, string>,\n key: string,\n templateName: string,\n): string {\n const value = vars[key];\n if (value === undefined) {\n throw new Error(`Template ${templateName}: missing variable '${key}'.`);\n }\n return value;\n}\n\nexport async function writeRendered(\n destDir: string,\n outputName: string,\n content: string,\n): Promise<void> {\n const target = join(destDir, outputName);\n // Defense in depth: unlink any pre-existing entry first so a planted\n // symlink at `target` cannot redirect the write into a host file.\n // The directory itself is checked separately in `init`.\n try {\n await unlink(target);\n } catch {\n // ENOENT, EISDIR, or any other failure: writeFile below will surface\n // a real problem with the path.\n }\n await writeFile(target, content, \"utf-8\");\n}\n\nexport function computeMarker(config: Config): string {\n const canonical = JSON.stringify({\n plugins: [...config.plugins].sort(),\n marketplaces: [...config.marketplaces].sort(),\n mcps: [...config.mcps]\n .map((m) => ({ name: m.name, transport: m.transport, url: m.url }))\n .sort((a, b) => a.name.localeCompare(b.name)),\n });\n // 16 hex chars = 64 bits. Birthday collisions need ~5 billion configs;\n // targeted preimage on truncated SHA-256 is computationally expensive\n // enough that the marker is not a useful attack surface.\n const hash = createHash(\"sha256\").update(canonical).digest(\"hex\").slice(0, 16);\n return `bootstrap-${hash}`;\n}\n\n// Compose project name comes from the directory containing the YAML, which\n// is `.sandbox-vibe` for every consumer of this CLI. Without further\n// scoping, two unrelated projects would share the `sandbox-home` volume\n// and leak Claude sessions, marketplace tokens, and installed plugins\n// between them. The slug isolates volumes per workspace; a SHA-8 of the\n// absolute workspace path disambiguates two projects that happen to share\n// the same basename (e.g. `~/work/foo` vs `~/play/foo`).\nexport function computeProjectSlug(config: Config): string {\n const raw = basename(config.workspacePath).toLowerCase();\n const cleaned = raw.replace(/[^a-z0-9-]+/g, \"-\").replace(/^-+|-+$/g, \"\");\n const safeBase = cleaned.length > 0 ? cleaned : \"vibe\";\n const pathHash = createHash(\"sha256\")\n .update(config.workspacePath)\n .digest(\"hex\")\n .slice(0, 8);\n return `${safeBase}-${pathHash}`;\n}\n\nexport function renderVolumesBlock(config: Config, projectSlug: string): string {\n const lines: string[] = [\n ` - ${projectSlug}-sandbox-home:/home/sandbox`,\n ` - ${config.workspacePath}:/workspace`,\n ];\n for (const m of config.additionalMounts) {\n const ro = m.readonly ? \":ro\" : \"\";\n lines.push(` - ${m.hostPath}:${m.containerPath}${ro}`);\n }\n return lines.join(\"\\n\");\n}\n\nexport function renderAdditionalDirsBlock(config: Config): string {\n const dirs: string[] = [\"/workspace\"];\n for (const m of config.additionalMounts) {\n dirs.push(m.containerPath);\n }\n return JSON.stringify(dirs, null, 2).replace(/\\n/g, \"\\n \");\n}\n\nexport function renderEnabledPluginsBlock(config: Config): string {\n const obj: Record<string, true> = {};\n for (const p of config.plugins) obj[p] = true;\n return JSON.stringify(obj, null, 2).replace(/\\n/g, \"\\n \");\n}\n\nexport function renderMarketplacesBlock(config: Config): string {\n if (config.marketplaces.length === 0) {\n return \"\";\n }\n // Output of `claude plugin marketplace add` may include a clone URL with\n // an embedded credential when the host has Git auth configured. Redirect\n // to the (chmod-600) bootstrap log instead of teeing into the agent's\n // stdout where the assistant could later read it via tool use.\n return config.marketplaces\n .map(\n (m) =>\n ` claude plugin marketplace add ${m} >>\"$$BOOT_LOG\" 2>&1`,\n )\n .join(\"\\n\");\n}\n\nexport function renderPluginLoopBlock(config: Config): string {\n if (config.plugins.length === 0) {\n return ` \"\"`;\n }\n return config.plugins\n .map((p, idx) => {\n const continuation = idx === config.plugins.length - 1 ? \"\" : \" \\\\\";\n return ` \"${p}\"${continuation}`;\n })\n .join(\"\\n\");\n}\n\nexport function renderMcpsBlock(config: Config): string {\n if (config.mcps.length === 0) {\n return \"\";\n }\n // MCP URLs can carry secrets in basic-auth or query parameters; redirect\n // command output to the chmod-600 bootstrap log instead of teeing.\n const lines = config.mcps.map(\n (m) =>\n ` claude mcp add ${m.name} --scope user --transport ${m.transport} ${m.url} >>\"$$BOOT_LOG\" 2>&1`,\n );\n return \"\\n\" + lines.join(\"\\n\") + \"\\n\";\n}\n\nexport function renderStackBlock(stack: Stack): string {\n switch (stack) {\n case \"none\":\n return \"# (no extra stack selected)\";\n case \"php\":\n return [\n \"# PHP intelephense (php-lsp plugin)\",\n \"RUN npm install -g intelephense\",\n ].join(\"\\n\");\n case \"dotnet\":\n return [\n \"# C# / .NET csharp-ls (csharp-lsp plugin)\",\n \"RUN apt-get update \\\\\",\n \" && apt-get install -y --no-install-recommends wget ca-certificates libicu72 \\\\\",\n \" && rm -rf /var/lib/apt/lists/* \\\\\",\n \" && wget -qO /tmp/dotnet-install.sh https://dot.net/v1/dotnet-install.sh \\\\\",\n \" && chmod +x /tmp/dotnet-install.sh \\\\\",\n \" && /tmp/dotnet-install.sh --channel 10.0 --install-dir /usr/share/dotnet \\\\\",\n \" && ln -s /usr/share/dotnet/dotnet /usr/local/bin/dotnet \\\\\",\n \" && rm /tmp/dotnet-install.sh \\\\\",\n \" && DOTNET_NOLOGO=1 DOTNET_CLI_TELEMETRY_OPTOUT=1 \\\\\",\n \" dotnet tool install --tool-path /usr/local/bin csharp-ls\",\n ].join(\"\\n\");\n case \"python\":\n return [\n \"# Python pyright (pyright-lsp plugin)\",\n \"RUN apt-get update \\\\\",\n \" && apt-get install -y --no-install-recommends python3-pip \\\\\",\n \" && rm -rf /var/lib/apt/lists/* \\\\\",\n \" && npm install -g pyright\",\n ].join(\"\\n\");\n case \"go\":\n // Force binaries into /usr/local/bin so the non-root `sandbox` user\n // can read+exec them (default `go install` location is /root/go/bin\n // which lives under root-only mode 700).\n return [\n \"# Go gopls (gopls-lsp plugin)\",\n \"RUN apt-get update \\\\\",\n \" && apt-get install -y --no-install-recommends golang-go \\\\\",\n \" && rm -rf /var/lib/apt/lists/* \\\\\",\n \" && GOBIN=/usr/local/bin go install golang.org/x/tools/gopls@latest \\\\\",\n \" && chmod a+rx /usr/local/bin/gopls\",\n ].join(\"\\n\");\n case \"rust\":\n // rustup-init writes to /root/.cargo (mode 700). Copy the binary the\n // plugin actually needs into /usr/local/bin so `sandbox` can use it.\n return [\n \"# Rust rust-analyzer (rust-analyzer-lsp plugin)\",\n \"RUN apt-get update \\\\\",\n \" && apt-get install -y --no-install-recommends rustup \\\\\",\n \" && rm -rf /var/lib/apt/lists/* \\\\\",\n \" && rustup-init -y --default-toolchain stable --no-modify-path \\\\\",\n \" && /root/.cargo/bin/rustup component add rust-analyzer \\\\\",\n \" && cp /root/.cargo/bin/rust-analyzer /usr/local/bin/rust-analyzer \\\\\",\n \" && chmod a+rx /usr/local/bin/rust-analyzer\",\n ].join(\"\\n\");\n }\n}\n\nexport async function renderAll(\n destDir: string,\n config: Config,\n): Promise<void> {\n const baseDockerfile = await renderTemplate(\n TEMPLATE_FILES.baseDockerfile.template,\n {},\n );\n await writeRendered(\n destDir,\n TEMPLATE_FILES.baseDockerfile.output,\n baseDockerfile,\n );\n\n const baseCompose = await renderTemplate(\n TEMPLATE_FILES.baseCompose.template,\n {\n cpus: String(config.resources.cpus),\n memoryGB: String(config.resources.memoryGB),\n pids: String(config.resources.pids),\n tmpfsMB: String(config.resources.tmpfsMB),\n },\n );\n await writeRendered(\n destDir,\n TEMPLATE_FILES.baseCompose.output,\n baseCompose,\n );\n\n const overrideDockerfile = await renderTemplate(\n TEMPLATE_FILES.overrideDockerfile.template,\n {\n stackBlock: renderStackBlock(config.stack),\n },\n );\n await writeRendered(\n destDir,\n TEMPLATE_FILES.overrideDockerfile.output,\n overrideDockerfile,\n );\n\n const projectSlug = computeProjectSlug(config);\n const overrideCompose = await renderTemplate(\n TEMPLATE_FILES.overrideCompose.template,\n {\n volumesBlock: renderVolumesBlock(config, projectSlug),\n additionalDirsBlock: renderAdditionalDirsBlock(config),\n enabledPluginsBlock: renderEnabledPluginsBlock(config),\n marketplacesBlock: renderMarketplacesBlock(config),\n pluginLoopBlock: renderPluginLoopBlock(config),\n mcpsBlock: renderMcpsBlock(config),\n marker: config.marker,\n projectSlug,\n },\n );\n await writeRendered(\n destDir,\n TEMPLATE_FILES.overrideCompose.output,\n overrideCompose,\n );\n}\n","import { loadConfig, saveConfig } from \"../config.js\";\nimport { log } from \"../log.js\";\nimport { findSandboxVibeDir, SANDBOX_VIBE_DIR } from \"../paths.js\";\nimport { renderAll } from \"../render.js\";\n\nexport async function bumpMarker(): Promise<void> {\n const cwd = process.cwd();\n const vibeDir = findSandboxVibeDir(cwd);\n if (!vibeDir) {\n throw new Error(\n `No ${SANDBOX_VIBE_DIR}/ found. Run sandbox-vibe init first.`,\n );\n }\n\n const config = loadConfig(vibeDir);\n const previous = config.marker;\n config.marker = nextMarker(previous);\n\n await renderAll(vibeDir, config);\n saveConfig(vibeDir, config);\n\n log(`Marker: ${previous} -> ${config.marker}`);\n log(\"Re-bootstrap on next up.\");\n}\n\nfunction nextMarker(current: string): string {\n const match = /^(.*)-r(\\d+)$/.exec(current);\n if (!match) return `${current}-r1`;\n // Both capture groups are guaranteed when match is truthy; defaults satisfy\n // noUncheckedIndexedAccess without a non-null assertion.\n const [, base = \"\", numStr = \"0\"] = match;\n return `${base}-r${Number.parseInt(numStr, 10) + 1}`;\n}\n","import {\n existsSync,\n lstatSync,\n mkdirSync,\n readFileSync,\n realpathSync,\n unlinkSync,\n writeFileSync,\n} from \"node:fs\";\nimport { basename, join, resolve } from \"node:path\";\nimport {\n type Config,\n DEFAULT_MARKETPLACES,\n DEFAULT_PLUGINS,\n DEFAULT_RESOURCES,\n saveConfig,\n validateContainerPath,\n validateHostPath,\n validateMcpName,\n validateMcpUrl,\n} from \"../config.js\";\nimport { log } from \"../log.js\";\nimport {\n findSandboxVibeDir,\n sandboxVibePath,\n SANDBOX_VIBE_DIR,\n} from \"../paths.js\";\nimport { checkbox, confirm, input, select } from \"../prompts.js\";\nimport { computeMarker, renderAll } from \"../render.js\";\nimport { isSensitivePath } from \"../sensitive-paths.js\";\n\nconst STACK_CHOICES = [\n { name: \"none\", value: \"none\" as const },\n { name: \"php (intelephense)\", value: \"php\" as const },\n { name: \"dotnet (csharp-ls)\", value: \"dotnet\" as const },\n { name: \"python (pyright)\", value: \"python\" as const },\n { name: \"go (gopls)\", value: \"go\" as const },\n { name: \"rust (rust-analyzer)\", value: \"rust\" as const },\n];\n\nasync function confirmSensitiveMount(absolutePath: string): Promise<boolean> {\n log(\n `WARNING: '${absolutePath}' looks like a system or credentials path. Mounting it exposes its contents to the Claude agent inside the container.`,\n );\n return confirm({\n message: \"Mount this path anyway?\",\n default: false,\n });\n}\n\nexport type InitOptions = {\n force?: boolean;\n nonInteractive?: boolean;\n};\n\nexport async function init(opts: InitOptions = {}): Promise<void> {\n const cwd = process.cwd();\n const existingDir = findSandboxVibeDir(cwd);\n\n if (existingDir && !opts.force) {\n if (opts.nonInteractive) {\n throw new Error(\n `${SANDBOX_VIBE_DIR}/ already exists. Use --force to overwrite.`,\n );\n }\n const overwrite = await confirm({\n message: `${SANDBOX_VIBE_DIR}/ already exists. Overwrite?`,\n default: false,\n });\n if (!overwrite) {\n log(\"Aborted by user.\");\n return;\n }\n }\n\n const config = opts.nonInteractive\n ? buildDefaultConfig(cwd)\n : await runWizard(cwd);\n config.marker = computeMarker(config);\n\n const destDir = sandboxVibePath(cwd);\n // Refuse to write through a symlinked .sandbox-vibe/. mkdir { recursive }\n // would silently no-op when the path exists, and the subsequent writeFile\n // calls would then follow the symlink (e.g. into /etc) and clobber host\n // files. lstat (not stat) inspects the link itself.\n try {\n if (lstatSync(destDir).isSymbolicLink()) {\n throw new Error(\n `${destDir} is a symbolic link; refusing to follow. Replace it with a regular directory and re-run.`,\n );\n }\n } catch (err) {\n if (\n err instanceof Error &&\n err.message.startsWith(`${destDir} is a symbolic link`)\n ) {\n throw err;\n }\n // Any other lstat error (typically ENOENT) is fine — mkdirSync below\n // will create the directory.\n }\n mkdirSync(destDir, { recursive: true });\n await renderAll(destDir, config);\n saveConfig(destDir, config);\n\n if (!opts.nonInteractive) {\n await maybeUpdateGitignore(cwd);\n }\n\n log(`Wrote ${SANDBOX_VIBE_DIR}/ with marker ${config.marker}.`);\n log(\"Run 'sandbox-vibe up' to start the sandbox.\");\n}\n\nfunction buildDefaultConfig(cwd: string): Config {\n return {\n schemaVersion: 1,\n workspacePath: cwd,\n additionalMounts: [],\n resources: { ...DEFAULT_RESOURCES },\n stack: \"none\",\n plugins: [...DEFAULT_PLUGINS],\n marketplaces: [...DEFAULT_MARKETPLACES],\n mcps: [],\n marker: \"\",\n };\n}\n\nasync function runWizard(cwd: string): Promise<Config> {\n const workspacePath = await promptHostPath(\n \"Workspace path (mounted as /workspace)\",\n cwd,\n );\n\n const additionalMounts = await promptAdditionalMounts();\n\n const stack = await select({\n message: \"Stack for LSP support\",\n choices: STACK_CHOICES,\n default: \"none\",\n });\n\n const plugins = await checkbox({\n message: \"Plugins to enable\",\n choices: DEFAULT_PLUGINS.map((p) => ({\n name: p,\n value: p,\n checked: true,\n })),\n });\n\n const mcps = await promptMcps();\n\n const useDefaults = await confirm({\n message: `Use default resources (${DEFAULT_RESOURCES.cpus} CPU, ${DEFAULT_RESOURCES.memoryGB}G mem, ${DEFAULT_RESOURCES.pids} PIDs, ${DEFAULT_RESOURCES.tmpfsMB}M tmpfs)?`,\n default: true,\n });\n const resources = useDefaults\n ? { ...DEFAULT_RESOURCES }\n : await promptResources();\n\n return {\n schemaVersion: 1,\n workspacePath,\n additionalMounts,\n resources,\n stack,\n plugins,\n marketplaces: [...DEFAULT_MARKETPLACES],\n mcps,\n marker: \"\",\n };\n}\n\nasync function promptHostPath(\n message: string,\n defaultValue?: string,\n): Promise<string> {\n while (true) {\n const raw = await input({\n message,\n default: defaultValue,\n validate: (value) => {\n const absolute = resolve(value);\n const formatCheck = validateHostPath(absolute);\n if (formatCheck !== true) return formatCheck;\n if (!existsSync(absolute)) return `Path does not exist: ${absolute}`;\n return true;\n },\n });\n const absolute = resolve(raw);\n // Resolve symlinks before the sensitive-path check so an attacker\n // cannot bypass it with `~/innocent-link -> /etc`. Docker mounts the\n // realpath anyway, so storing the resolved path keeps config and\n // runtime behaviour aligned.\n let real: string;\n try {\n real = realpathSync(absolute);\n } catch {\n log(`Cannot resolve real path for ${absolute}; please pick another.`);\n continue;\n }\n if (isSensitivePath(real)) {\n const ok = await confirmSensitiveMount(real);\n if (!ok) {\n log(\"Aborted mount; please pick another path.\");\n continue;\n }\n }\n return real;\n }\n}\n\nasync function promptAdditionalMounts(): Promise<Config[\"additionalMounts\"]> {\n const mounts: Config[\"additionalMounts\"] = [];\n let addMore = await confirm({\n message: \"Add sibling mounts?\",\n default: false,\n });\n while (addMore) {\n const hostPath = await promptHostPath(\"Host path (absolute)\");\n const defaultContainerPath = `/workspace/${basename(hostPath)}`;\n const containerPath = await input({\n message: \"Container path\",\n default: defaultContainerPath,\n validate: (value) => validateContainerPath(value),\n });\n const readonly = await confirm({\n message: \"Read-only?\",\n default: false,\n });\n mounts.push({ hostPath, containerPath, readonly });\n addMore = await confirm({\n message: \"Add another mount?\",\n default: false,\n });\n }\n return mounts;\n}\n\nasync function promptMcps(): Promise<Config[\"mcps\"]> {\n const mcps: Config[\"mcps\"] = [];\n const addMcp = await confirm({\n message: \"Add MCP servers?\",\n default: false,\n });\n if (!addMcp) return mcps;\n\n let more = true;\n while (more) {\n const name = await input({\n message: \"MCP name\",\n default: \"context7\",\n validate: (value) => validateMcpName(value),\n });\n const url = await input({\n message: \"MCP URL\",\n default:\n name === \"context7\" ? \"https://mcp.context7.com/mcp\" : \"\",\n validate: (value) => validateMcpUrl(value),\n });\n mcps.push({ name, transport: \"http\", url });\n more = await confirm({\n message: \"Add another MCP?\",\n default: false,\n });\n }\n return mcps;\n}\n\nasync function promptResources(): Promise<Config[\"resources\"]> {\n const cpus = await promptPositiveNumber(\n \"CPU limit (count)\",\n DEFAULT_RESOURCES.cpus,\n false,\n );\n const memoryGB = await promptPositiveNumber(\n \"Memory limit (GB)\",\n DEFAULT_RESOURCES.memoryGB,\n false,\n );\n const pids = await promptPositiveNumber(\n \"PID limit\",\n DEFAULT_RESOURCES.pids,\n true,\n );\n const tmpfsMB = await promptPositiveNumber(\n \"tmpfs /tmp size (MB)\",\n DEFAULT_RESOURCES.tmpfsMB,\n true,\n );\n return { cpus, memoryGB, pids, tmpfsMB };\n}\n\nasync function promptPositiveNumber(\n message: string,\n defaultValue: number,\n integer: boolean,\n): Promise<number> {\n const raw = await input({\n message,\n default: String(defaultValue),\n validate: (v) => {\n const n = Number(v);\n if (!Number.isFinite(n) || n <= 0) return \"must be > 0\";\n if (integer && !Number.isInteger(n)) return \"must be an integer\";\n return true;\n },\n });\n return Number(raw);\n}\n\nasync function maybeUpdateGitignore(cwd: string): Promise<void> {\n const gitignorePath = join(cwd, \".gitignore\");\n const entry = `/${SANDBOX_VIBE_DIR}/`;\n\n // lstat (not stat) so we see the symlink itself, not its target.\n // Refusing to follow a symlink prevents an attacker who controls the\n // working directory (e.g. a malicious git checkout) from redirecting\n // our writeFile into /etc/hosts or any other host file.\n let isSymlink = false;\n let exists = false;\n try {\n const st = lstatSync(gitignorePath);\n exists = true;\n isSymlink = st.isSymbolicLink();\n } catch {\n // ENOENT or other stat error: treat as \"does not exist\".\n }\n\n if (isSymlink) {\n throw new Error(\n `.gitignore at ${gitignorePath} is a symbolic link; refusing to follow. Replace it with a regular file and re-run.`,\n );\n }\n\n if (!exists) {\n const create = await confirm({\n message: `No .gitignore found. Create one with '${entry}'?`,\n default: true,\n });\n if (create) {\n writeGitignoreSafely(gitignorePath, entry + \"\\n\");\n log(\"Created .gitignore.\");\n }\n return;\n }\n\n const content = readFileSync(gitignorePath, \"utf-8\");\n if (content.split(/\\r?\\n/).some((line) => line.trim() === entry)) {\n return;\n }\n const add = await confirm({\n message: `Add '${entry}' to .gitignore?`,\n default: true,\n });\n if (add) {\n const trailing = content.endsWith(\"\\n\") ? \"\" : \"\\n\";\n writeGitignoreSafely(gitignorePath, content + trailing + entry + \"\\n\");\n log(\"Updated .gitignore.\");\n }\n}\n\n// Closes the TOCTTOU window between the earlier `lstatSync` check and the\n// actual write: an attacker who creates a symlink at `gitignorePath`\n// during that gap would otherwise see writeFileSync follow the link.\n// `unlinkSync` removes a symlink without dereferencing it, and the\n// subsequent `writeFileSync` creates a fresh regular file.\nfunction writeGitignoreSafely(gitignorePath: string, content: string): void {\n try {\n unlinkSync(gitignorePath);\n } catch {\n // ENOENT or any other condition where unlink is a no-op; writeFileSync\n // below will surface a real failure.\n }\n writeFileSync(gitignorePath, content, \"utf-8\");\n}\n\n","import {\n input,\n select,\n confirm,\n checkbox,\n Separator,\n} from \"@inquirer/prompts\";\n\nexport { input, select, confirm, checkbox, Separator };\n\n// Inquirer rejects the prompt promise with `ExitPromptError` when the user\n// presses Ctrl+C, and with `AbortPromptError` when an attached AbortSignal\n// fires (timeout, manual cancel). Both should exit cleanly without a stack\n// trace. Other Error subclasses are real failures and must propagate.\nexport function isAbortError(err: unknown): boolean {\n if (!(err instanceof Error)) return false;\n return err.name === \"ExitPromptError\" || err.name === \"AbortPromptError\";\n}\n","import { homedir } from \"node:os\";\n\n// Mounting any of these paths exposes host credentials, secrets, or system\n// state to the agent inside the container (which runs `claude\n// --dangerously-skip-permissions` against `additionalDirectories`). The\n// wizard does not block them — it asks for explicit confirmation so a\n// careless mount is not accepted by default. Detection is exported so\n// it can be exercised by tests without going through the inquirer wizard.\n// Includes both the conventional Linux locations and the macOS firmlinks\n// that `realpathSync` resolves to (e.g. `/etc` -> `/private/etc`). Without\n// the `/private/*` entries the sensitive check silently passes through on\n// macOS after realpath resolution.\nexport const SENSITIVE_SYSTEM_PATHS: readonly string[] = [\n \"/\",\n \"/etc\",\n \"/private/etc\",\n \"/root\",\n \"/var\",\n \"/private/var\",\n \"/sys\",\n \"/proc\",\n \"/usr\",\n \"/boot\",\n \"/dev\",\n];\n\nexport const SENSITIVE_HOME_SUBDIRS: readonly string[] = [\n \".ssh\",\n \".aws\",\n \".gnupg\",\n \".docker\",\n \".kube\",\n \".config/gh\",\n \".npmrc\",\n];\n\nexport function isSensitivePath(absolutePath: string): boolean {\n if (SENSITIVE_SYSTEM_PATHS.includes(absolutePath)) return true;\n for (const sys of SENSITIVE_SYSTEM_PATHS) {\n // Treat any subpath of a system root (e.g. `/etc/foo`) the same way.\n if (sys !== \"/\" && absolutePath.startsWith(`${sys}/`)) return true;\n }\n const home = homedir();\n for (const suffix of SENSITIVE_HOME_SUBDIRS) {\n const sensitive = `${home}/${suffix}`;\n if (absolutePath === sensitive || absolutePath.startsWith(`${sensitive}/`)) {\n return true;\n }\n }\n return false;\n}\n","import { ExecaError, execa } from \"execa\";\nimport { join } from \"node:path\";\n\nexport async function assertDockerAvailable(): Promise<void> {\n try {\n await execa(\"docker\", [\"info\"], {\n timeout: 3000,\n stdio: \"ignore\",\n });\n } catch (err) {\n if (err instanceof ExecaError && err.timedOut) {\n throw new Error(\n \"Docker daemon did not respond within 3 seconds. Is Docker Desktop / colima running?\",\n );\n }\n throw new Error(\n \"Docker daemon not reachable. Start Docker Desktop / colima and retry.\",\n );\n }\n\n let composeVersion: string;\n try {\n const result = await execa(\"docker\", [\"compose\", \"version\", \"--short\"], {\n timeout: 3000,\n });\n composeVersion = result.stdout.trim();\n } catch (err) {\n if (err instanceof ExecaError && err.timedOut) {\n throw new Error(\n \"`docker compose version` did not respond within 3 seconds.\",\n );\n }\n throw new Error(\n \"Requires Docker Compose v2 (docker compose). Could not detect version.\",\n );\n }\n\n const normalized = composeVersion.startsWith(\"v\")\n ? composeVersion.slice(1)\n : composeVersion;\n const majorPart = normalized.split(\".\")[0] ?? \"\";\n const major = Number.parseInt(majorPart, 10);\n if (!Number.isFinite(major) || major < 2) {\n throw new Error(\n `Requires Docker Compose v2 or newer. Found '${composeVersion}'; please upgrade.`,\n );\n }\n}\n\nfunction composeFlags(vibeDir: string): string[] {\n return [\n \"compose\",\n \"-f\",\n join(vibeDir, \"docker-compose.sandbox.yml\"),\n \"-f\",\n join(vibeDir, \"docker-compose.override.yml\"),\n ];\n}\n\nexport async function composeBuild(vibeDir: string): Promise<void> {\n // The override's `FROM sandbox-vibe-base:latest` depends on the base image\n // existing locally. Build the base alone first, then the override on top.\n await execa(\n \"docker\",\n [\n \"compose\",\n \"-f\",\n join(vibeDir, \"docker-compose.sandbox.yml\"),\n \"build\",\n ],\n { stdio: \"inherit\" },\n );\n await execa(\"docker\", [...composeFlags(vibeDir), \"build\"], {\n stdio: \"inherit\",\n });\n}\n\nexport async function composeRun(vibeDir: string): Promise<void> {\n await execa(\n \"docker\",\n [...composeFlags(vibeDir), \"run\", \"--rm\", \"sandbox\"],\n {\n stdio: \"inherit\",\n },\n );\n}\n","import {\n assertDockerAvailable,\n composeBuild,\n composeRun,\n} from \"../docker.js\";\nimport { log } from \"../log.js\";\nimport { findSandboxVibeDir, SANDBOX_VIBE_DIR } from \"../paths.js\";\n\nexport async function up(): Promise<void> {\n const cwd = process.cwd();\n const vibeDir = findSandboxVibeDir(cwd);\n if (!vibeDir) {\n throw new Error(\n `No ${SANDBOX_VIBE_DIR}/ found in current directory. Run 'sandbox-vibe init' first or cd into the project root.`,\n );\n }\n\n await assertDockerAvailable();\n log(\"Building images...\");\n await composeBuild(vibeDir);\n log(\"Starting Claude REPL...\");\n await composeRun(vibeDir);\n}\n"],"mappings":"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","names":["readFileSync","homedir","dirname","join","fileURLToPath","join","join","readFileSync","unlinkSync","writeFileSync","basename","join","resolve","absolute","resolve","basename","join","readFileSync","unlinkSync","writeFileSync","join","join","dirname","fileURLToPath","readFileSync","homedir"]}
@@ -0,0 +1,44 @@
1
+ // src/sensitive-paths.ts
2
+ import { homedir } from "os";
3
+ var SENSITIVE_SYSTEM_PATHS = [
4
+ "/",
5
+ "/etc",
6
+ "/private/etc",
7
+ "/root",
8
+ "/var",
9
+ "/private/var",
10
+ "/sys",
11
+ "/proc",
12
+ "/usr",
13
+ "/boot",
14
+ "/dev"
15
+ ];
16
+ var SENSITIVE_HOME_SUBDIRS = [
17
+ ".ssh",
18
+ ".aws",
19
+ ".gnupg",
20
+ ".docker",
21
+ ".kube",
22
+ ".config/gh",
23
+ ".npmrc"
24
+ ];
25
+ function isSensitivePath(absolutePath) {
26
+ if (SENSITIVE_SYSTEM_PATHS.includes(absolutePath)) return true;
27
+ for (const sys of SENSITIVE_SYSTEM_PATHS) {
28
+ if (sys !== "/" && absolutePath.startsWith(`${sys}/`)) return true;
29
+ }
30
+ const home = homedir();
31
+ for (const suffix of SENSITIVE_HOME_SUBDIRS) {
32
+ const sensitive = `${home}/${suffix}`;
33
+ if (absolutePath === sensitive || absolutePath.startsWith(`${sensitive}/`)) {
34
+ return true;
35
+ }
36
+ }
37
+ return false;
38
+ }
39
+ export {
40
+ SENSITIVE_HOME_SUBDIRS,
41
+ SENSITIVE_SYSTEM_PATHS,
42
+ isSensitivePath
43
+ };
44
+ //# sourceMappingURL=sensitive-paths.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/sensitive-paths.ts"],"sourcesContent":["import { homedir } from \"node:os\";\n\n// Mounting any of these paths exposes host credentials, secrets, or system\n// state to the agent inside the container (which runs `claude\n// --dangerously-skip-permissions` against `additionalDirectories`). The\n// wizard does not block them — it asks for explicit confirmation so a\n// careless mount is not accepted by default. Detection is exported so\n// it can be exercised by tests without going through the inquirer wizard.\n// Includes both the conventional Linux locations and the macOS firmlinks\n// that `realpathSync` resolves to (e.g. `/etc` -> `/private/etc`). Without\n// the `/private/*` entries the sensitive check silently passes through on\n// macOS after realpath resolution.\nexport const SENSITIVE_SYSTEM_PATHS: readonly string[] = [\n \"/\",\n \"/etc\",\n \"/private/etc\",\n \"/root\",\n \"/var\",\n \"/private/var\",\n \"/sys\",\n \"/proc\",\n \"/usr\",\n \"/boot\",\n \"/dev\",\n];\n\nexport const SENSITIVE_HOME_SUBDIRS: readonly string[] = [\n \".ssh\",\n \".aws\",\n \".gnupg\",\n \".docker\",\n \".kube\",\n \".config/gh\",\n \".npmrc\",\n];\n\nexport function isSensitivePath(absolutePath: string): boolean {\n if (SENSITIVE_SYSTEM_PATHS.includes(absolutePath)) return true;\n for (const sys of SENSITIVE_SYSTEM_PATHS) {\n // Treat any subpath of a system root (e.g. `/etc/foo`) the same way.\n if (sys !== \"/\" && absolutePath.startsWith(`${sys}/`)) return true;\n }\n const home = homedir();\n for (const suffix of SENSITIVE_HOME_SUBDIRS) {\n const sensitive = `${home}/${suffix}`;\n if (absolutePath === sensitive || absolutePath.startsWith(`${sensitive}/`)) {\n return true;\n }\n }\n return false;\n}\n"],"mappings":";AAAA,SAAS,eAAe;AAYjB,IAAM,yBAA4C;AAAA,EACvD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEO,IAAM,yBAA4C;AAAA,EACvD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEO,SAAS,gBAAgB,cAA+B;AAC7D,MAAI,uBAAuB,SAAS,YAAY,EAAG,QAAO;AAC1D,aAAW,OAAO,wBAAwB;AAExC,QAAI,QAAQ,OAAO,aAAa,WAAW,GAAG,GAAG,GAAG,EAAG,QAAO;AAAA,EAChE;AACA,QAAM,OAAO,QAAQ;AACrB,aAAW,UAAU,wBAAwB;AAC3C,UAAM,YAAY,GAAG,IAAI,IAAI,MAAM;AACnC,QAAI,iBAAiB,aAAa,aAAa,WAAW,GAAG,SAAS,GAAG,GAAG;AAC1E,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;","names":[]}
@@ -0,0 +1,20 @@
1
+ # Override layer generated by sandbox-vibe.
2
+ # Edit only with full understanding — this file may be regenerated by 'sandbox-vibe init'.
3
+
4
+ FROM sandbox-vibe-base:latest
5
+
6
+ USER root
7
+
8
+ # Always: npm + Claude Code
9
+ RUN npm install -g npm@latest \
10
+ && npm install -g @anthropic-ai/claude-code@latest
11
+
12
+ # vibe-render:stackBlock
13
+
14
+ USER sandbox
15
+
16
+ # Useful env vars when .NET is installed (silences extra noise).
17
+ # Keeping them without .NET causes no side effect.
18
+ ENV DOTNET_NOLOGO=1 \
19
+ DOTNET_CLI_TELEMETRY_OPTOUT=1 \
20
+ DOTNET_ROOT=/usr/share/dotnet
@@ -0,0 +1,21 @@
1
+ # sandbox-vibe base image.
2
+ # Kept intentionally minimal: only universally useful tooling, nothing else.
3
+ # Anything stack-specific (PHP intelephense, .NET SDK, Flutter, etc.)
4
+ # goes in your project's Dockerfile.sandbox.override.
5
+
6
+ FROM node:24-slim
7
+
8
+ RUN apt-get update && apt-get install -y --no-install-recommends \
9
+ git \
10
+ curl \
11
+ ca-certificates \
12
+ openssh-client \
13
+ python3 \
14
+ unzip \
15
+ zip \
16
+ && rm -rf /var/lib/apt/lists/*
17
+
18
+ RUN useradd -m -s /bin/bash sandbox
19
+ USER sandbox
20
+
21
+ CMD ["bash"]
@@ -0,0 +1,56 @@
1
+ # Override generated by sandbox-vibe.
2
+ # Edit only with full understanding — this file may be regenerated by 'sandbox-vibe init'.
3
+
4
+ services:
5
+ sandbox:
6
+ image: sandbox-vibe:latest
7
+ build:
8
+ context: .
9
+ dockerfile: Dockerfile.sandbox.override
10
+ volumes:
11
+ ${volumesBlock}
12
+
13
+ entrypoint:
14
+ - /bin/bash
15
+ - -c
16
+ - |
17
+ set -e
18
+ set -o pipefail
19
+
20
+ mkdir -p ~/.claude
21
+ cat > ~/.claude/settings.json << 'EOF'
22
+ {
23
+ "permissions": {
24
+ "additionalDirectories": ${additionalDirsBlock}
25
+ },
26
+ "enabledPlugins": ${enabledPluginsBlock}
27
+ }
28
+ EOF
29
+
30
+ if [ ! -f ~/.claude/.${marker} ]; then
31
+ BOOT_LOG=/tmp/sandbox-bootstrap.log
32
+ : > "$$BOOT_LOG"
33
+ chmod 600 "$$BOOT_LOG"
34
+ echo "[sandbox] First run: installing marketplaces, plugins and MCPs..."
35
+
36
+ ${marketplacesBlock}
37
+
38
+ for p in \
39
+ ${pluginLoopBlock}; do
40
+ echo "[sandbox] install: $$p"
41
+ claude plugin install "$$p" >>"$$BOOT_LOG" 2>&1
42
+ done
43
+ ${mcpsBlock}
44
+ touch ~/.claude/.${marker}
45
+ echo "[sandbox] Bootstrap done. Log at $$BOOT_LOG (chmod 600)."
46
+ fi
47
+
48
+ if [ -t 0 ]; then
49
+ exec claude --dangerously-skip-permissions
50
+ else
51
+ echo "[sandbox] No interactive TTY; exiting after bootstrap. Use 'docker compose ... run --rm sandbox' in a terminal."
52
+ fi
53
+ command: []
54
+
55
+ volumes:
56
+ ${projectSlug}-sandbox-home:
@@ -0,0 +1,29 @@
1
+ # sandbox-vibe tracked compose.
2
+ # Defines the base image and the security/resource limits that apply to any project.
3
+ # Project volumes, plugins, MCPs and a custom entrypoint go in docker-compose.override.yml.
4
+
5
+ services:
6
+ sandbox:
7
+ image: sandbox-vibe-base:latest
8
+ build:
9
+ context: .
10
+ dockerfile: Dockerfile.sandbox
11
+ working_dir: /workspace
12
+ stdin_open: true
13
+ tty: true
14
+ deploy:
15
+ resources:
16
+ limits:
17
+ cpus: "${cpus}"
18
+ memory: ${memoryGB}G
19
+ pids: ${pids}
20
+ reservations:
21
+ cpus: "1"
22
+ memory: 512M
23
+ tmpfs:
24
+ - /tmp:size=${tmpfsMB}M
25
+ security_opt:
26
+ - no-new-privileges:true
27
+ cap_drop:
28
+ - ALL
29
+ network_mode: bridge
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "sandbox-vibe",
3
+ "version": "0.1.0",
4
+ "description": "Plug-and-play Docker sandbox for Claude Code with idempotent plugin and MCP bootstrap and security limits enforced by default",
5
+ "type": "module",
6
+ "bin": {
7
+ "sandbox-vibe": "dist/cli.mjs"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "engines": {
13
+ "node": ">=20.0.0"
14
+ },
15
+ "scripts": {
16
+ "build": "tsup",
17
+ "dev": "tsup --watch",
18
+ "prepublishOnly": "npm run build"
19
+ },
20
+ "dependencies": {
21
+ "@inquirer/prompts": "^8.4.2",
22
+ "commander": "^14.0.3",
23
+ "execa": "^9.5.0"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^25.6.2",
27
+ "tsup": "^8.3.0",
28
+ "typescript": "^6.0.3"
29
+ },
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/navegar-sistemas/sandbox-vibe.git",
34
+ "directory": "cli"
35
+ },
36
+ "homepage": "https://github.com/navegar-sistemas/sandbox-vibe#readme",
37
+ "bugs": {
38
+ "url": "https://github.com/navegar-sistemas/sandbox-vibe/issues"
39
+ },
40
+ "keywords": [
41
+ "docker",
42
+ "claude-code",
43
+ "sandbox",
44
+ "ai",
45
+ "cli",
46
+ "agent",
47
+ "isolation"
48
+ ],
49
+ "publishConfig": {
50
+ "access": "public"
51
+ }
52
+ }