Skip to content

Commit 55a36fc

Browse files
authored
Add Podman support (#12)
* Add Podman support * Add tests
1 parent 4237d50 commit 55a36fc

File tree

9 files changed

+411
-4
lines changed

9 files changed

+411
-4
lines changed

.claude/settings.json

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(mkdir:*)",
5+
"Bash(npm run build:*)",
6+
"Bash(grep:*)",
7+
"Bash(ls:*)",
8+
"Bash(docker logs:*)",
9+
"Bash(docker kill:*)",
10+
"Bash(docker rm:*)",
11+
"Bash(npx tsc:*)",
12+
"Bash(npm install:*)",
13+
"Bash(node:*)",
14+
"Bash(timeout:*)",
15+
"Bash(true)",
16+
"Bash(docker stop:*)",
17+
"Bash(mv:*)",
18+
"Bash(curl:*)",
19+
"WebFetch(domain:localhost)",
20+
"Bash(pkill:*)",
21+
"Bash(docker exec:*)",
22+
"Bash(npx ts-node:*)",
23+
"Bash(docker pull:*)",
24+
"Bash(rg:*)",
25+
"Bash(npm start)",
26+
"Bash(find:*)",
27+
"Bash(npm run lint)",
28+
"Bash(sed:*)",
29+
"Bash(npx claude-sandbox purge:*)",
30+
"Bash(docker cp:*)",
31+
"Bash(npm run test:e2e:*)",
32+
"Bash(gh pr list:*)",
33+
"Bash(kill:*)",
34+
"Bash(npm start:*)",
35+
"Bash(npm run purge-containers:*)",
36+
"Bash(claude-sandbox start:*)",
37+
"Bash(npm run lint)"
38+
],
39+
"deny": []
40+
},
41+
"enableAllProjectMcpServers": false
42+
}

README.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ npm install -g @textcortex/claude-code-sandbox
4444
### Prerequisites
4545

4646
- Node.js >= 18.0.0
47-
- Docker
47+
- Docker or Podman
4848
- Git
4949
- Claude Code (`npm install -g @anthropic-ai/claude-code@latest`)
5050

@@ -218,6 +218,7 @@ Create a `claude-sandbox.config.json` file (see `claude-sandbox.config.example.j
218218
- `bashTimeout`: Timeout for bash commands in milliseconds
219219
- `containerPrefix`: Custom prefix for container names
220220
- `claudeConfigPath`: Path to Claude configuration file
221+
- `dockerSocketPath`: Custom Docker/Podman socket path (auto-detected by default)
221222

222223
#### Mount Configuration
223224

@@ -236,6 +237,27 @@ Example use cases:
236237

237238
## Features
238239

240+
### Podman Support
241+
242+
Claude Code Sandbox now supports Podman as an alternative to Docker. The tool automatically detects whether you're using Docker or Podman by checking for available socket paths:
243+
244+
- **Automatic detection**: The tool checks for Docker and Podman sockets in standard locations
245+
- **Custom socket paths**: Use the `dockerSocketPath` configuration option to specify a custom socket
246+
- **Environment variable**: Set `DOCKER_HOST` to override socket detection
247+
248+
Example configuration for Podman:
249+
250+
```json
251+
{
252+
"dockerSocketPath": "/run/user/1000/podman/podman.sock"
253+
}
254+
```
255+
256+
The tool will automatically detect and use Podman if:
257+
258+
- Docker socket is not available
259+
- Podman socket is found at standard locations (`/run/podman/podman.sock` or `$XDG_RUNTIME_DIR/podman/podman.sock`)
260+
239261
### Web UI Terminal
240262

241263
Launch a browser-based terminal interface to interact with Claude Code:

claude-sandbox.config.example.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,6 @@
2727
"maxThinkingTokens": 100000,
2828
"bashTimeout": 600000,
2929
"containerPrefix": "claude-code-sandbox",
30-
"claudeConfigPath": "~/.claude.json"
30+
"claudeConfigPath": "~/.claude.json",
31+
"dockerSocketPath": null
3132
}

src/cli.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,37 @@ import Docker from "dockerode";
66
import { ClaudeSandbox } from "./index";
77
import { loadConfig } from "./config";
88
import { WebUIServer } from "./web-server";
9+
import { getDockerConfig, isPodman } from "./docker-config";
910
import ora from "ora";
1011

11-
const docker = new Docker();
12+
// Initialize Docker with config - will be updated after loading config if needed
13+
let dockerConfig = getDockerConfig();
14+
let docker = new Docker(dockerConfig);
1215
const program = new Command();
1316

17+
// Helper function to reinitialize Docker with custom socket path
18+
function reinitializeDocker(socketPath?: string) {
19+
if (socketPath) {
20+
dockerConfig = getDockerConfig(socketPath);
21+
docker = new Docker(dockerConfig);
22+
23+
// Log if using Podman
24+
if (isPodman(dockerConfig)) {
25+
console.log(chalk.blue("Detected Podman socket"));
26+
}
27+
}
28+
}
29+
30+
// Helper to ensure Docker is initialized with config
31+
async function ensureDockerConfig() {
32+
try {
33+
const config = await loadConfig("./claude-sandbox.config.json");
34+
reinitializeDocker(config.dockerSocketPath);
35+
} catch (error) {
36+
// Config loading failed, continue with default Docker config
37+
}
38+
}
39+
1440
// Helper function to get Claude Sandbox containers
1541
async function getClaudeSandboxContainers() {
1642
const containers = await docker.listContainers({ all: true });
@@ -123,6 +149,7 @@ program
123149
.command("attach [container-id]")
124150
.description("Attach to an existing Claude Sandbox container")
125151
.action(async (containerId) => {
152+
await ensureDockerConfig();
126153
const spinner = ora("Looking for containers...").start();
127154

128155
try {
@@ -169,6 +196,7 @@ program
169196
.description("List all Claude Sandbox containers")
170197
.option("-a, --all", "Show all containers (including stopped)")
171198
.action(async (options) => {
199+
await ensureDockerConfig();
172200
const spinner = ora("Fetching containers...").start();
173201

174202
try {
@@ -211,6 +239,7 @@ program
211239
.description("Stop Claude Sandbox container(s)")
212240
.option("-a, --all", "Stop all Claude Sandbox containers")
213241
.action(async (containerId, options) => {
242+
await ensureDockerConfig();
214243
const spinner = ora("Stopping containers...").start();
215244

216245
try {
@@ -272,6 +301,7 @@ program
272301
.option("-n, --tail <lines>", "Number of lines to show from the end", "50")
273302
.action(async (containerId, options) => {
274303
try {
304+
await ensureDockerConfig();
275305
let targetContainerId = containerId;
276306

277307
if (!targetContainerId) {
@@ -310,6 +340,7 @@ program
310340
.description("Remove all stopped Claude Sandbox containers")
311341
.option("-f, --force", "Remove all containers (including running)")
312342
.action(async (options) => {
343+
await ensureDockerConfig();
313344
const spinner = ora("Cleaning up containers...").start();
314345

315346
try {
@@ -346,6 +377,7 @@ program
346377
.option("-y, --yes", "Skip confirmation prompt")
347378
.action(async (options) => {
348379
try {
380+
await ensureDockerConfig();
349381
const containers = await getClaudeSandboxContainers();
350382

351383
if (containers.length === 0) {

src/docker-config.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import * as fs from "fs";
2+
import * as path from "path";
3+
4+
interface DockerConfig {
5+
socketPath?: string;
6+
}
7+
8+
/**
9+
* Detects whether Docker or Podman is available and returns appropriate configuration
10+
* @param customSocketPath - Optional custom socket path from configuration
11+
*/
12+
export function getDockerConfig(customSocketPath?: string): DockerConfig {
13+
// Allow override via environment variable
14+
if (process.env.DOCKER_HOST) {
15+
return {}; // dockerode will use DOCKER_HOST automatically
16+
}
17+
18+
// Use custom socket path if provided
19+
if (customSocketPath) {
20+
return { socketPath: customSocketPath };
21+
}
22+
23+
// Common socket paths to check
24+
const socketPaths = [
25+
// Docker socket paths
26+
"/var/run/docker.sock",
27+
28+
// Podman rootless socket paths
29+
process.env.XDG_RUNTIME_DIR &&
30+
path.join(process.env.XDG_RUNTIME_DIR, "podman", "podman.sock"),
31+
`/run/user/${process.getuid?.() || 1000}/podman/podman.sock`,
32+
33+
// Podman root socket path
34+
"/run/podman/podman.sock",
35+
].filter(Boolean) as string[];
36+
37+
// Check each socket path
38+
for (const socketPath of socketPaths) {
39+
try {
40+
if (fs.existsSync(socketPath)) {
41+
const stats = fs.statSync(socketPath);
42+
if (stats.isSocket()) {
43+
return { socketPath };
44+
}
45+
}
46+
} catch (error) {
47+
// Socket might exist but not be accessible, continue checking
48+
continue;
49+
}
50+
}
51+
52+
// No socket found, return empty config and let dockerode use its defaults
53+
return {};
54+
}
55+
56+
/**
57+
* Checks if we're using Podman based on the socket path
58+
*/
59+
export function isPodman(config: DockerConfig): boolean {
60+
return config.socketPath?.includes("podman") ?? false;
61+
}

src/index.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { ContainerManager } from "./container";
77
import { UIManager } from "./ui";
88
import { WebUIServer } from "./web-server";
99
import { SandboxConfig } from "./types";
10+
import { getDockerConfig, isPodman } from "./docker-config";
1011
import path from "path";
1112

1213
export class ClaudeSandbox {
@@ -21,7 +22,14 @@ export class ClaudeSandbox {
2122

2223
constructor(config: SandboxConfig) {
2324
this.config = config;
24-
this.docker = new Docker();
25+
const dockerConfig = getDockerConfig(config.dockerSocketPath);
26+
this.docker = new Docker(dockerConfig);
27+
28+
// Log if using Podman
29+
if (isPodman(dockerConfig)) {
30+
console.log(chalk.blue("Detected Podman socket"));
31+
}
32+
2533
this.git = simpleGit();
2634
this.credentialManager = new CredentialManager();
2735
this.gitMonitor = new GitMonitor(this.git);

src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export interface SandboxConfig {
2525
targetBranch?: string;
2626
remoteBranch?: string;
2727
prNumber?: string;
28+
dockerSocketPath?: string;
2829
}
2930

3031
export interface Credentials {

0 commit comments

Comments
 (0)