npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

mac-sandbox

v1.0.1

Published

Create and manage sandboxes on your Mac with a simple CLI and API. Using Apple Containers and Cloudflare Tunnels

Readme

mac-sandbox

npm i mac-sandbox -g
sandbox start

Or, to expose your API publicly via a Cloudflare Tunnel:

sandbox start -t

Self-hosted sandbox containers on Apple Silicon Macs, with Cloudflare Tunnel routing and a pairing-based auth flow. Built with Hono, TypeScript, and Apple Containers.

Note: This library is still actively being worked on. If you have feedback or run into issues, feel free to DM @jerrickhakim.

Why this was created

Most sandbox solutions are cloud-hosted and charge per execution. This project creates an API layer that lets you self-host sandboxes on hardware you already own — your Mac. If you have an Apple Silicon Mac sitting around, you can run isolated, ephemeral containers locally and expose them publicly through Cloudflare Tunnels, with no third-party sandbox provider needed.


How it works

Your Mac (port 4000)
  ├── Sandbox API server            ← you talk to this
  └── Apple Container(s)            ← your image, your app
  • The Sandbox API runs on your Mac at port 4000 and manages container lifecycle.
  • Each sandbox runs your own container image — bring whatever stack you need.
  • Containers can be exposed publicly via Cloudflare Tunnels (named or free trycloudflare.com).

Prerequisites


Installation

Install the CLI

npm install -g mac-sandbox

Build your container image

Build any Linux container image that suits your workload and tag it:

container build -t sandbox:latest /path/to/your/Dockerfile

Then point the CLI at it:

sandbox start --image my-image:latest

Verify available images:

container image list

If the build fails, run: container system kernel set --recommended --force


CLI Reference

sandbox <command> [options]

COMMANDS:
  start         Start the sandbox server (auto-installs dependencies)
  setup         Install required dependencies (cloudflared, container CLI)
  health        Check system dependencies
  pair          Generate a new pairing code
  tokens        List all active auth tokens
  list          List all running containers
  delete        Delete all containers
  destroy       Stop container system and delete all containers + volumes

OPTIONS:
  --port, -p        Port to run the server on (default: 4000)
  --tunnel, -t      Enable Cloudflare tunnel (generates a public QR code URL)
  --image, -i       Container image name (default: sandbox)
  --skip-setup      Skip dependency check on startup

Quick start

Local network mode (default):

sandbox start

Prints a QR code, a 6-digit pairing code, and the local network URL (e.g. http://192.168.1.100:4000). Scan the QR code or POST the code to /pairing/confirm from any device on the same network.

Tunnel mode:

sandbox start -t

Same as above, but the QR code points to a public trycloudflare.com URL so remote devices can pair.


Pairing Flow

1. sandbox start                  → server starts, displays QR code + 6-digit code
2. Client scans QR or POSTs code  → POST /pairing/confirm
3. Server returns Bearer token    → client stores it
4. Token used on all API calls    → Authorization: Bearer <token>

Confirm pairing (public — no auth required)

POST /pairing/confirm
Content-Type: application/json

{
  "code": "ABC123",
  "deviceName": "My Laptop"   // optional
}

Response:

{ "token": "tok_secret..." }

Environment Variables

Server (runtime)

| Variable | Default | Description | | -------------------------------- | --------- | ------------------------------------------------------------------------------- | | PORT | 4000 | Port the sandbox API server listens on | | SANDBOX_IMAGE | sandbox | Container image name used when spawning sandboxes | | FLAGS_DESTROY_CONTAINERS | — | Set "true" to stop & delete all containers on server shutdown | | FLAGS_PRESERVE_SANDBOX_TUNNELS | — | Set "true" to also tear down per-sandbox Cloudflare tunnels on shutdown | | AUTO_INSTALL_CONTAINER_PKG | — | Set "1" to run sudo installer silently instead of opening the GUI installer |

Cloudflare (optional — only for named tunnels)

Skip these if you use free trycloudflare.com tunnels and pass flags.tunnel: false when creating sandboxes.

| Variable | Description | | --------------------------- | -------------------------------------------- | | CLOUDFLARE_TUNNEL_API_KEY | Cloudflare API token with tunnel permissions | | CLOUDFLARE_ACCOUNT_ID | Cloudflare account ID | | CLOUDFLARE_ZONE_ID | Cloudflare zone ID for your domain |


Sandbox API

All sandbox endpoints require Authorization: Bearer <token>.

Create sandbox

POST /sandbox/create
{
  "cpus": 2,
  "memory": "4G",
  "storage": "10G",
  "volumeId": "existing-volume-id",
  "flags": {
    "tunnel": false,
    "lan": true
  },
  "healthCheck": {
    "url": "tunnel",
    "timeout": 5000,
    "interval": 1000,
    "maxAttempts": 10,
    "destroy": true
  },
  "env": {
    "MY_VAR": "my-value"
  }
}

env is an arbitrary key/value map passed as environment variables to your container. Use it however your image expects.

storage options: 1G 5G 10G 15G 20G 25G 30G 32G 64G 128G

Response:

{
  "id": "abc123",
  "ipAddress": "192.168.64.5",
  "urls": {
    "tunnel": "https://abc.trycloudflare.com",
    "lan": "http://192.168.1.100:8080",
    "container": "http://192.168.64.5:80"
  }
}

Other sandbox endpoints

| Method | Endpoint | Description | | -------- | ---------------------- | ------------------------------------------------------------------ | | GET | /sandbox/list | List all sandboxes | | GET | /sandbox/:id | Get sandbox details | | GET | /sandbox/:id/health | Check container reachability | | GET | /sandbox/:id/inspect | Full inspection: sandbox + container + volume info | | DELETE | /sandbox/:id | Stop and remove sandbox (?preserveStorage=true keeps the volume) |

Attach a Cloudflare tunnel

POST /sandbox/:id/tunnel
{
  "tunnelToken": "<from-cloudflare>",
  "tunnelId": "<tunnel-uuid>",
  "url": "https://abc123.yourdomain.com",
  "checkhealth": true
}

Execute a command

POST /sandbox/:id/exec
{
  "command": "npm",
  "args": ["run", "build"],
  "timeout": 30000,
  "user": "node"
}

Response:

{
  "success": true,
  "exitCode": 0,
  "stdout": "...",
  "stderr": "",
  "durationMs": 1234
}

Execute with streaming output (SSE)

POST /sandbox/:id/exec/stream

Same request body as /exec. Returns a Server-Sent Events stream with events:

| Event | Payload | | -------- | ------------------------------------- | | stdout | Base64-encoded stdout chunk | | stderr | Base64-encoded stderr chunk | | exit | { exitCode, signal } | | error | { error } | | ping | { timestamp } (keepalive every 15s) |


Volume API

Persistent volumes can be attached to sandboxes at /workspace.

| Method | Endpoint | Description | | -------- | ---------------------- | ----------------------- | | POST | /volume/create | Create a volume | | GET | /volume/list | List all volumes | | GET | /volume/:id | Get volume details | | DELETE | /volume/:id | Delete a volume | | POST | /volume/batch-delete | Delete multiple volumes |

Create a volume

POST /volume/create
{
  "id": "my-volume",
  "size": "10G",
  "label": "my-label"
}

Attach a volume to a sandbox

Pass volumeId in the create sandbox request to mount an existing volume at /workspace:

{ "volumeId": "my-volume", ... }

Or pass storage: "10G" to create and mount a new volume automatically.


API Documentation

The server serves interactive API docs:

  • Scalar UI: http://localhost:4000/reference
  • OpenAPI JSON: http://localhost:4000/doc

Server Integration Example

Minimal backend code to create a sandbox and wire up a Cloudflare tunnel:

import axios from "axios";

const api = axios.create({
  baseURL: "https://xyz.trycloudflare.com", // from pairing
  headers: { Authorization: `Bearer ${platformToken}` },
});

// 1. Create sandbox
const { data } = await api.post("/sandbox/create", {
  cpus: 2,
  memory: "4G",
  env: {
    // pass whatever env vars your image expects
    MY_VAR: "my-value",
  },
});

const { id: sandboxId, ipAddress } = data;

// 2. Create a named Cloudflare tunnel + CNAME pointing to the container
const { tunnelId, tunnelToken } = await createCloudflareTunnel({
  hostname: `${sandboxId}.yourdomain.com`,
  localServiceUrl: `http://${ipAddress}:80`,
});

// 3. Send tunnel credentials to Mac so cloudflared starts in the container
await api.post(`/sandbox/${sandboxId}/tunnel`, {
  tunnelToken,
  tunnelId,
  url: `https://${sandboxId}.yourdomain.com`,
  checkhealth: true,
});

Type Definitions

interface SandboxConfig {
  cpus?: number;
  memory?: string;
  storage?: "1G" | "5G" | "10G" | "15G" | "20G" | "25G" | "30G" | "32G" | "64G" | "128G" | null;
  volumeId?: string;
  flags?: { tunnel?: boolean; lan?: boolean };
  healthCheck?: {
    url?: "tunnel" | "lan" | "container";
    timeout?: number;
    interval?: number;
    maxAttempts?: number;
    destroy?: boolean;
  };
  env?: Record<string, string>;
}

interface SandboxEntry {
  id: string;
  ipAddress: string;
  hostPort: number;
  createdAt: number;
  volume: string | null;
  urls: {
    tunnel: string | null;
    lan: string | null;
    container: string;
  };
}

interface ExecRequest {
  command: string;
  args?: string[];
  timeout?: number;
  user?: string;
}

interface ExecResponse {
  success: boolean;
  command: string;
  args?: string[];
  exitCode: number;
  stdout: string;
  stderr: string;
  durationMs: number;
}

Troubleshooting

| Issue | Solution | | ---------------------------------------------- | --------------------------------------------------- | | Container build fails | container system kernel set --recommended --force | | Build export error: "structure needs cleaning" | See Build Export Error below | | Stuck containers | sandbox restart or sandbox destroy | | cloudflared not found | sandbox setup | | Container system not running | container system start |

Build export error

If you see Error: failed to write compressed diff: lstat /tmp/containerd-mount*: structure needs cleaning:

# 1. Clean stale mounts
sudo rm -rf /tmp/containerd-mount*

# 2. Restart container system
container system stop && container system start

# 3. Retry build without cache
container build --no-cache -t sandbox:latest .

If it still fails:

container system kernel set --recommended --force
container system start
container build -t sandbox:latest .

License

MIT