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

sandwrap

v0.0.3

Published

Run commands in a sandbox with filesystem isolation using bubblewrap

Readme

sandwrap

A simple CLI tool that runs any command inside a bubblewrap + overlayfs sandbox, preventing it from modifying your actual filesystem. After the command exits, you can review diffs and selectively apply or discard changes.

Usage

bunx sandwrap <command> [args...]

# Examples
bunx sandwrap claude
bunx sandwrap claude code
bunx sandwrap ./my-agent.sh
bunx sandwrap npm run dangerous-script

How It Works

┌─────────────────────────────────────────────────────────────┐
│                      sandwrap process                       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. Setup Phase                                             │
│     ┌─────────────────────────────────────────────────┐     │
│     │  Create temp directory structure:               │     │
│     │    /tmp/sandwrap-XXXX/                          │     │
│     │      ├── upper/    (overlay writes go here)    │     │
│     │      ├── work/     (overlayfs workdir)         │     │
│     │      └── merged/   (union mount point)         │     │
│     └─────────────────────────────────────────────────┘     │
│                                                             │
│  2. Execution Phase                                         │
│     ┌─────────────────────────────────────────────────┐     │
│     │  bwrap invocation:                              │     │
│     │    - Mount CWD as lower (read-only)            │     │
│     │    - Mount upper + lower as overlay            │     │
│     │    - Bind essential dirs (/usr, /bin, etc.)    │     │
│     │    - Run <command> inside sandbox              │     │
│     │    - All writes captured in upper/             │     │
│     └─────────────────────────────────────────────────┘     │
│                                                             │
│  3. Review Phase (after command exits)                      │
│     ┌─────────────────────────────────────────────────┐     │
│     │  - Scan upper/ for changes                      │     │
│     │  - Generate diffs for modified files           │     │
│     │  - Show new/deleted files                       │     │
│     │  - Interactive prompt: apply/discard/review    │     │
│     └─────────────────────────────────────────────────┘     │
│                                                             │
└─────────────────────────────────────────────────────────────┘

CLI Interface

Main Command

sandwrap <command> [args...]

Options:
  --no-network, -n     Disable network access inside sandbox
  --keep, -k           Keep overlay directory after exit (for debugging)
  --auto-apply, -y     Apply all changes without prompting
  --auto-discard, -d   Discard all changes without prompting
  --help, -h           Show help
  --version, -v        Show version

Post-Execution Review

After the sandboxed command exits, sandwrap enters interactive review mode:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  sandwrap: command exited with code 0
  
  Changes detected:
    M  src/index.ts        (+42, -13)
    M  package.json        (+2, -1)
    A  src/utils/new.ts    (+87)
    D  old-config.json
    
  Total: 2 modified, 1 added, 1 deleted
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

What would you like to do?

  [a] Apply all changes
  [d] Discard all changes  
  [r] Review changes interactively
  [s] Select files to apply
  [q] Quit (discard all)

> 

Interactive Review Mode (r)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  Reviewing: src/index.ts (1/4)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

--- a/src/index.ts
+++ b/src/index.ts
@@ -10,6 +10,12 @@ import { foo } from './foo';
 
 export function main() {
+  // Added safety check
+  if (!validateInput(input)) {
+    throw new Error('Invalid input');
+  }
+
   const result = process(input);
   return result;
 }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  [y] Apply this file  [n] Skip  [v] View full file  
  [e] Edit before applying  [q] Quit review
> 

Select Files Mode (s)

Select files to apply (space to toggle, enter to confirm):

  [x] M  src/index.ts
  [ ] M  package.json  
  [x] A  src/utils/new.ts
  [ ] D  old-config.json

> 

Architecture

sandwrap/
├── package.json
├── src/
│   ├── index.ts          # Entry point & CLI parsing
│   ├── sandbox.ts        # bwrap + overlay setup/teardown
│   ├── diff.ts           # Change detection & diff generation
│   ├── review.ts         # Interactive review UI
│   └── apply.ts          # File operations to apply changes
└── README.md

Core Module: sandbox.ts

Responsible for:

  1. Creating overlay structure

    interface SandboxContext {
      id: string;
      tempDir: string;
      upperDir: string;   // Where writes go
      workDir: string;    // OverlayFS requirement
      mergedDir: string;  // Union mount point
      targetDir: string;  // Original CWD being sandboxed
    }
  2. Building bwrap command

    bwrap \
      --ro-bind /usr /usr \
      --ro-bind /lib /lib \
      --ro-bind /lib64 /lib64 \
      --ro-bind /bin /bin \
      --ro-bind /etc /etc \
      --dev /dev \
      --proc /proc \
      --tmpfs /tmp \
      --bind $UPPER $CWD \        # Overlay writes go to upper
      --ro-bind $CWD $CWD/.lower  # Read-only access to original
      --chdir $CWD \
      --unshare-user \
      --unshare-pid \
      --unshare-net \             # If --no-network
      --die-with-parent \
      -- <command> [args...]
  3. Alternative: fuse-overlayfs for unprivileged users

    If user lacks permissions for kernel overlayfs, fall back to fuse-overlayfs:

    fuse-overlayfs -o lowerdir=$CWD,upperdir=$UPPER,workdir=$WORK $MERGED

Core Module: diff.ts

Responsible for detecting and formatting changes:

interface FileChange {
  path: string;
  type: 'added' | 'modified' | 'deleted';
  diff?: string;           // Unified diff for text files
  linesAdded?: number;
  linesRemoved?: number;
  isBinary: boolean;
}

// Scan upper directory for changes
function detectChanges(ctx: SandboxContext): FileChange[];

// Generate unified diff between original and modified
function generateDiff(original: string, modified: string): string;

Change Detection Logic

OverlayFS marks changes in the upper directory:

  • New files: Present in upper, not in lower
  • Modified files: Present in both (upper shadows lower)
  • Deleted files: Character device with 0/0 major/minor (whiteout)
    // Detect whiteout files (overlayfs deletion markers)
    const stats = await fs.lstat(path);
    const isWhiteout = stats.isCharacterDevice() && 
                       stats.rdev === 0;

Core Module: review.ts

Interactive terminal UI using something like @clack/prompts or raw readline:

interface ReviewResult {
  filesToApply: string[];
  filesToDiscard: string[];
}

async function reviewChanges(changes: FileChange[]): Promise<ReviewResult>;

Core Module: apply.ts

Applies selected changes from upper directory to original:

async function applyChanges(
  ctx: SandboxContext, 
  files: string[]
): Promise<void> {
  for (const file of files) {
    const src = path.join(ctx.upperDir, file);
    const dst = path.join(ctx.targetDir, file);
    
    const stats = await fs.lstat(src);
    
    if (isWhiteout(stats)) {
      // Delete from original
      await fs.rm(dst, { recursive: true });
    } else {
      // Copy from upper to original
      await fs.cp(src, dst, { recursive: true });
    }
  }
}

Dependencies

{
  "name": "sandwrap",
  "version": "0.1.0",
  "bin": {
    "sandwrap": "./dist/index.js"
  },
  "dependencies": {
    "@clack/prompts": "^0.7.0",
    "diff": "^5.2.0",
    "picocolors": "^1.0.0"
  },
  "devDependencies": {
    "typescript": "^5.0.0",
    "@types/node": "^20.0.0",
    "@types/diff": "^5.0.0"
  }
}

Requirements

System dependencies (must be installed):

  • bwrap (bubblewrap) - Usually available as bubblewrap package
  • fuse-overlayfs (optional fallback for unprivileged overlay)

Check on startup:

async function checkDependencies(): Promise<void> {
  try {
    await execAsync('which bwrap');
  } catch {
    console.error('Error: bubblewrap not found.');
    console.error('Install with: sudo apt install bubblewrap');
    process.exit(1);
  }
}

Edge Cases & Considerations

  1. Binary files: Show as changed but don't display diff content
  2. Symlinks: Preserve symlink targets when applying
  3. Permissions: Preserve file modes when copying
  4. Large files: Stream diff rather than loading into memory
  5. Nested sandboxing: Detect if already in sandbox and warn/fail
  6. Signal handling: Clean up overlay on SIGINT/SIGTERM
  7. Unprivileged users: Fall back to fuse-overlayfs if needed
  8. macOS: bubblewrap is Linux-only; would need alternative (maybe lima/colima + bwrap inside)

Example Session

$ bunx sandwrap claude

# ... claude runs, makes changes ...
# User types /exit or Ctrl+D

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  sandwrap: claude exited with code 0
  
  Changes detected:
    M  src/api.ts           (+15, -3)
    M  src/types.ts         (+8, -0)
    A  src/helpers/cache.ts (+45)
    
  Total: 2 modified, 1 added, 0 deleted
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

What would you like to do?

  [a] Apply all changes
  [d] Discard all changes  
  [r] Review changes interactively
  [s] Select files to apply
  [q] Quit (discard all)

> r

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  Reviewing: src/api.ts (1/3)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

--- a/src/api.ts
+++ b/src/api.ts
@@ -22,6 +22,18 @@ export async function fetchData(id: string) {
+  // Add caching layer
+  const cached = await cache.get(id);
+  if (cached) return cached;
+
   const response = await fetch(`/api/data/${id}`);
-  return response.json();
+  const data = await response.json();
+  await cache.set(id, data);
+  return data;
 }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  [y] Apply  [n] Skip  [v] View full  [q] Quit
> y

✓ Applied src/api.ts
✓ Applied src/types.ts  
✓ Applied src/helpers/cache.ts

Done! 3 files applied, 0 discarded.

Future Enhancements

  • --snapshot mode: Save overlay as tarball for later replay
  • --compare mode: Run same command with/without changes, compare output
  • Git integration: Stage applied changes automatically
  • Undo support: Keep backup of original files before applying
  • Config file: .sandwraprc for default options per project