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

worm-cli

v0.2.0

Published

Manage warm, parallel git worktree environments for AI coding agents.

Readme

wormhole

A hub for your coding agents — one git-tracked home for their config and instructions, the tooling to maintain it, and a warm pool of worktrees to run several at once.

A coding agent is only as good as its setup: the right instructions and skills, an allowlist of commands it may run unattended, a sandbox around the ones you don't fully trust, and room to work without colliding with the next agent. That setup is usually ad-hoc, trapped on one machine, and untracked — and it falls apart the moment you run more than one agent on a project.

worm turns it into a managed, version-controlled layer with three parts:

  • A meta-repo for your agents — ~/.worm. A single git repository holds the config and instructions for every project's agents: CLAUDE.md, skills, slash commands, settings, the permission allowlist, the sandbox policy. Because it's just git, your agent setup is reviewable, shareable, and portable — re-clone a machine and your agents come back configured.
  • Tooling to maintain that config — recipes. Composable capabilities you switch on per project: a Docker sandbox that keeps filesystem-mutating commands off the host, permission sync so approving a command in one slot teaches every agent, shared history across worktrees. worm sync reconciles everything declaratively; templates seed new projects.
  • Tooling to run agents in parallel — a warm worktree pool. Permanent git worktree slots let several agents work different branches at once without trampling each other. Think of them as parallel universes of your repo: your main clone is Slot 0, siblings are <repo>-1, <repo>-2, … Each universe is isolated, always-warm (no re-npm install per task), and wired with the same shared config via tunnels - this is where wormhole gets its name from.

The worktree pool used to be the whole story; now it's the backend. The product is the cognitive layer it serves — the git-tracked home that keeps your agents consistent, contained, and reproducible across every universe.

Built around Claude Code today (the recipes wire Claude's hooks and settings) on a deliberately agent-agnostic core — the pool, tunnels, and lifecycle hooks don't care which agent you run. Opening the recipe set and wiring other agents are the next iterations — see Roadmap.

Video - Overview & Demo

(in French, English version incoming!)


Install

npm install -g worm-cli    # exposes the `worm` binary

Requires Node ≥ 20 and git ≥ 2.31.

pnpm install
pnpm build
pnpm link --global    # exposes the `worm` binary

Shell integration (recommended)

Add this to your ~/.zshrc (or ~/.bashrc):

eval "$(worm shell-init)"
eval "$(worm completion zsh)"    # or `bash`
  • worm shell-init installs a worm() wrapper so worm cd <branch> / worm tp <N> actually change your shell's working directory.
  • worm completion <shell> registers tab completion: subcommand names (worm sta<tab>worm status), branch completion for switch, and slot/branch completion for cd / tp / path.

Quick start

# Clone a repo and bind it as Slot 0 (a normal clone — no bare container).
worm clone https://github.com/you/mkpc.git ~/git/mkpc
cd ~/git/mkpc

# Day to day, just switch branches in place — Slot 0 stays warm.
git switch my-feature        # or: worm switch my-feature   (also re-runs setup.sh)

# Want a second branch checked out at the same time? Add a permanent universe.
worm universe add my-other-feature        # creates ~/git/mkpc-1, runs setup.sh
worm cd my-other-feature                   # hop into it

# See the whole pool.
worm status

# Done with a universe? Collapse it (Slot 0 can never be removed).
worm universe rm my-other-feature

worm sync reconciles your shared-file tunnels across every slot (run it after editing shared_paths). worm sync --global does the HOME-scope equivalent and installs any global recipes (autosync) from ~/.worm/config.json into ~/.claude/settings.json. Drop your install commands into .worm/scripts/setup.sh — it runs on worm universe add and worm switch.

Commands

| Command | What it does | |---|---| | worm clone <url> [path] [--name X] [--template <dir>] [--skip-hook] | Recommended entry point. Normal-clones <url>, binds it as Slot 0, and warms it via on_create. | | worm init [--name X] [--template <dir>] [--skip-hook] | Bind the current git clone as Slot 0 and warm it via on_create. Lazily creates ~/.worm/ on first use. Idempotent. | | worm status [--json] | List every slot in the pool (Slot 0 + siblings) and the branch each is on. | | worm universe add <branch> [--create] [--skip-hook] | Create a permanent sibling worktree on <branch> at <repo>-<N>, link shared paths, run on_create. Refuses a branch already checked out in another slot. If <branch> doesn't exist, prompts to create it (--create skips the prompt). | | worm universe rm <ref> [--force] [--skip-hook] | Remove a sibling universe — <ref> is a slot index or a branch. Refuses Slot 0; refuses uncommitted changes unless --force; runs on_remove. | | worm switch <branch> [--create] [--skip-hook] | git switch <branch> in the current slot and re-run the warm-up hook. If <branch> doesn't exist, prompts to create it (--create skips the prompt). (Plain git switch works too — this just adds the hook + the "branch held elsewhere" guard.) | | worm sync | Declaratively reconcile shared-path links across all slots: create missing tunnels, prune removed ones, clone any missing store. Idempotent. | | worm wire [path] | Apply the cognitive layer (shared-path tunnels, the env file, recipe hooks) to a worktree worm didn't create (default: cwd). Call it from another tool's worktree-create hook (Conductor, worktrunk, plain git worktree) to use worm without adopting its topology. Idempotent. | | worm detach <file> | Sever a shared-path tunnel in the current worktree only — replace the symlink with an independent local copy. The other slots keep the link; worm sync won't restore it. Reverse it by deleting the local file and re-running worm sync. | | worm sync --global | Reconcile HOME-scope links from ~/.worm/config.json's shared_paths (e.g. ~/.claude/commands~/.worm/shared/.claude/commands), and install/strip global recipes (autosync) into ~/.claude/settings.json — machine-wide setup, independent of any project. | | worm template render <file> [KEY=VALUE …] | Render a {{var}} template file to stdout (worm's templating primitive; leaves shell ${VAR} untouched). For setup scripts that want to drop hand-rolled sed. | | worm cd <branch> / worm tp <N> | Change directory into a slot by branch name or 0-based index. Requires the shell-init wrapper. | | worm path <ref> | Print the worktree path for a branch or slot index (what cd/tp use under the hood). | | worm destroy [--force] | Unbind the project: remove sibling universes, .worm/, and the global profile. Slot 0 (your repo) is left intact. Prompts unless --force. | | worm shell-init | Print the shell function described in Shell integration. | | worm completion <bash\|zsh> | Print a tab-completion script for the chosen shell. |

Run worm <command> --help for the full option list. Project-scoped commands resolve Slot 0 via git (--git-common-dir), so they work from any slot or subdirectory.

How it works

For a project named mkpc with two extra universes:

~/git/
├── mkpc/                              ← Slot 0: the primary working tree (a normal clone)
│   ├── .git/                          ← standard git directory (the common dir for all slots)
│   ├── .worm/                         ← thin wiring: (almost) all pointers (git-excluded locally)
│   │   ├── .gitignore                 ← single line: `*`
│   │   ├── config.json                → ~/.worm/projects/mkpc/config.json
│   │   ├── scripts/                   → ~/.worm/projects/mkpc/scripts/   (setup.sh lives here)
│   │   ├── recipes/                   → ~/.worm/projects/mkpc/recipes/   (materialized artifacts)
│   │   └── logs/                      → ~/.worm/projects/mkpc/logs/      (recipe-hook logs)
│   └── .env                           → ~/.worm/projects/mkpc/.env       (a tunnel, on Slot 0)
├── mkpc-1/                            ← sibling universe (worm universe add …)
│   └── .env                           → ~/.worm/projects/mkpc/.env
└── mkpc-2/                            ← another sibling universe

~/.worm/                              ← your agents' meta-repo (a git repo)
└── projects/mkpc/                 ← mkpc's PROFILE — the durable state, survives a reclone
    ├── config.json  .env  scripts/  recipes/  logs/
    └── .managed-links.json           ← manifest of the symlinks worm created per slot

Slots are permanent — there's no spawn/teardown, so they stay warm (your node_modules, build state, etc. just persist in each one). Sibling worktrees live one level up so Slot 0's git status never sees them, and .worm/ is hidden from git locally. A project's .worm/ is almost entirely symlinks into its profile (~/.worm/projects/<project>/), where the durable state lives; each slot's shared files link straight at the profile (absolute, one hop), recorded in the profile's manifest so worm sync can add/prune them safely.

Configuration

Each project gets a config at ~/.worm/projects/<project-name>/config.json. The built-in default is intentionally minimal — projects start with no shared files, so worm doesn't presume your stack:

{
  "shared_paths": [],
  "stores": {},
  "hooks": {
    "on_create": "bash \"$WORM_PROJECT_ROOT/.worm/scripts/setup.sh\""
  },
  "recipes": {}
}

Edit the file (or pre-seed a --template <dir>) to add what your project needs.

💡 A complete real-world example: worm-mkpc is the full profile behind the demo videoconfig.json, setup.sh, a per-worktree env block, the recipe set, and the docker/CLAUDE.local.md templates for a real project. A good setup to crib from.

  • shared_paths — files tunnelled into every slot. Each entry is either a bare path, pulled from the project profile (~/.worm/projects/<project>/<path>, sprouting an empty placeholder if absent), or { "path": ".claude/docs", "store": "team" } to pull it from a named store instead. Each slot gets an absolute symlink straight at the source. Common entries: .env, CLAUDE.local.md, .mcp.json. Run worm sync after changing this list.

  • stores — named external sources for shared_paths, e.g. { "team": { "root": "~/git/team-shared", "url": "[email protected]:org/team-shared" } }. A shared_paths entry with "store": "team" links from that store's root instead of the profile — so team docs/commands can live in a separate git repo, shared with your team and editable in place (your edits land as changes in that repo). If root is missing and a url is given, worm sync clones it on demand. Declare stores per project here, or machine-wide in ~/.worm/config.json (project stores win on a name clash).

  • env — a per-worktree dotenv file (off unless present). Unlike shared_paths (one source symlinked identically everywhere), env writes a different file into each slot. Shape: { "file": ".env.worm", "vars": { "FRONT": "{{ 3000 + index * 10000 }}" } }. file (default .env.worm) is the generated filename, gitignored automatically. Each vars value is an integer arithmetic expression (+ - * / %, parentheses) over two offset bases — pick the one that fits:

    • index — the slot number (positional). {{ 3000 + index * 10000 }}3000, 13000, 23000. Clean and sequential; stable for a fixed pool, but drifts if worktrees are ephemeral (the slot number isn't tied to the branch).
    • offset / hash — derived from a stable hash of the branch. {{ 8080 + offset }} gives the same port for a given branch on any machine and any slot order (offset ∈ 0–999), at the cost of non-sequential values. The ephemeral-worktree-safe choice.

    Plus the text vars {{ slot }} / {{ branch }} (e.g. DB_NAME=app_{{ slot }}). Multiple components just share the same basis: FRONT={{ 3000 + index * 10000 }}, BACK={{ 3001 + index * 10000 }}. Regenerated on init, universe add, switch, and sync (rewritten only when the content changes), and may not also appear in shared_paths (worm refuses the clash). For advanced cases (a real config file with holes) keep using worm template render in setup.sh; env is the zero-file-to-maintain path for the common one.

  • hookson_create runs inside a slot to warm it up: when Slot 0 is bound (init / clone), when a sibling is created (universe add), and on switch. on_remove runs before a slot is removed. The default on_create invokes .worm/scripts/setup.sh — drop your install commands there (npm install, pip install -r requirements.txt, …) instead of editing the JSON. A non-zero on_create warns but doesn't abort; a non-zero on_remove aborts the removal unless --force. Pass --skip-hook to any of these commands to bind/switch without running the hook (e.g. on an already-warm checkout).

  • recipes — composable capabilities, keyed by name (provider-style). A recipe is enabled iff its key is present; each value is validated by that recipe's own schema. Two kinds of thing back a recipe, kept deliberately separate:

    • Worm-owned code ships with the binary — config-independent scripts (the sandbox interceptor, the permission-sync script) parameterized at run time, so they live once and a fix propagates by upgrading worm, with nothing to re-materialize per project.
    • Genuinely per-project artifacts (the sandbox Dockerfile / compose.yml / policy) are materialized under .worm/recipes/<name>/ (→ the profile), non-clobbering — edit a generated file and it's kept. To pull a scaffold update, delete it (e.g. rm .worm/recipes/sandbox/Dockerfile) and re-run worm sync. (A planned regenerate/upgrade command is the last roadmap item — see docs/recipes-roadmap.md.)

    Hooks are inverted: each slot's .claude/settings.local.json (gitignored, per-slot) holds one static entry per eventworm hook trigger <event> — installed once. At tool/session time that dispatcher resolves the live slot, runs each enabled recipe's command for the event, injects env, and owns logging. So enabling, disabling, or updating a recipe is a pure config.json edit — settings never churn, and recipes compose without clobbering each other or your own hooks. Recipe-hook logs land in .worm/logs/ (→ the profile): <container>.log for the container's up/down output, <container>-redirect.log for the sandbox's allow/deny decisions, sync-permissions.log for permission sync. tail -f them to see what fired.

    Built-in recipes (enable by adding the key, e.g. "recipes": { "sandbox": {}, "syncPermissions": {} }). Most are project-scoped (per-project config, wired per-slot); autosync, notifyPendingInput and syncGlobalPermissions are global — declare them in ~/.worm/config.json and run worm sync --global (they wire into ~/.claude/settings.json and fire for every Claude session, any project):

    • sandbox{ "backend": "docker", "image": "node:22-bookworm", "tools": [], "neverSandbox": [...], "exemptDirs": [], "autostart": true, "autostop": false }. Materializes a Dockerfile (from image + tools), a compose file, and a sandbox policy (the interceptor itself ships with worm), then wires each slot so its container auto-starts (autostart) and filesystem-mutating commands are redirected into it (mounted at the same path via $SANDBOX_DIR). See docs/strategy-3-spec.md §6.
    • syncPermissions ({}) — wires SessionStart/SessionEnd hooks that union the permissions block of each slot's settings.local.json with a canonical store shared across slots (approve a command once, every slot learns it). Merge-preserving — it never touches other recipes' hooks.
    • shareHistory ({}) — symlinks each sibling slot's Claude history dir (~/.claude/projects/<slot-slug>) to Slot 0's, so all slots share one conversation history. Refuses to clobber a real history dir (warns instead).
    • shareMemory ({}) — symlinks every slot's Claude memory dir (~/.claude/projects/<slot-slug>/memory) at one canonical store in the profile (~/.worm/projects/<name>/.claude/memory), so all slots share one memory that's durable across a Slot 0 reclone. Because the store is the profile (not Slot 0), Slot 0 is linked too; on first run it seeds the store from an existing memory dir, and refuses to clobber a real one once the store exists (warns instead).
    • autosync ({ "remote": "origin", "debounceMinutes": 5, "notify": true }) — the one global-scope recipe: declare it in the global ~/.worm/config.json ("recipes": { "autosync": {} }) and run worm sync --global to wire it into ~/.claude/settings.json, so it fires for every Claude session machine-wide (any project, even non-worm dirs) — not per-slot. It keeps the ~/.worm meta-repo synced across machines without manual push/pull on every node: pull (fetch + rebase) on SessionStart, push (auto-commit + push, debounced) on every turn's Stop — the reliable trigger for a session that's never closed — plus a best-effort flush on SessionEnd. Runs are serialized by a machine-local lock (many open windows all firing SessionStart/Stop won't run git on ~/.worm concurrently — a busy run just skips; the lock carries an owner token so a stale-steal can't hand it to two racers). Each sync commits local work before rebasing onto the remote — a committed change survives a conflict abort (back on HEAD), whereas an autostash pop-conflict would strand it invisibly. Conflicts are never auto-resolved: a clean git rebase --abort, a durable marker that worm status surfaces, and an OS notification (notify: false to silence). Requires a git remote on ~/.worm (else it no-ops). Machine-local state (.managed-links.json, logs, the conflict marker) is gitignored so it never syncs. Remove it from the config + re-run worm sync --global to uninstall.
    • notifyPendingInput ({ "openOnClick": "" }, global) — fires an OS notification when Claude is waiting on you (your input is pending): "Response ready" on Stop, "Waiting for approval" on PermissionRequest. On macOS it uses terminal-notifier and falls back to osascript; on Linux, notify-send. Set openOnClick to any open -a app name ("Visual Studio Code", "Cursor", "Windsurf", …) to make clicking the notification open the project folder there; default "" = no click action (no editor is presumed). Debounces the burst of intermediate Stop yields a background-agent turn produces (one notification on the final answer) and ignores sub-agent events. Shares the notification backend (src/recipes/_lib/notify.js) with autosync.
    • syncGlobalPermissions ({}, global) — the machine-wide analogue of syncPermissions: bidirectionally unions the permissions block of ~/.claude/settings.json with a git-tracked canonical copy in ~/.worm/shared/.claude/settings.json, so your global allow/deny rules are version-controlled (and synced across machines once autosync pushes them). Only permissions is touched — hooks, marketplaces, trusted dirs in the live file are left intact.

Hook environment

Hook commands (and any script they invoke, like setup.sh) receive:

| Variable | Value | |---|---| | WORM_PROJECT_ROOT | Absolute path to Slot 0 (the primary working tree). | | WORM_SLOT | Slot name being acted on (main for Slot 0, <N> for siblings). | | WORM_SLOT_INDEX | The numeric, 0-based slot index. Handy for derived values: PORT=$((8080 + WORM_SLOT_INDEX)) (positional — stable only for a fixed pool). | | WORM_BRANCH | Branch name. | | WORM_WORKTREE | This slot's worktree path (equals WORM_PROJECT_ROOT for Slot 0). | | WORM_BRANCH_HASH | Stable 32-bit hash of the branch — same value across machines and slot reordering. | | WORM_PORT_OFFSET | Stable per-branch offset in 0–999. PORT=$((8080 + WORM_PORT_OFFSET)) is the ephemeral-worktree-safe alternative to WORM_SLOT_INDEX (same basis as the env block's {{ offset }}). |

Templates

On first run, ~/.worm/templates/default/ is seeded with a config.json and scripts/setup.sh. New projects are bootstrapped from that template — edits to it apply to projects created afterwards (existing projects are untouched). Pass --template <dir> to bootstrap from a custom directory; it must contain a config.json, with an optional scripts/ subdirectory.

Environment

| Variable | Effect | |---|---| | WORM_HOME | Override the global root (default: ~/.worm). Useful for sandboxes, CI, or multiple worm "homes". | | WORM_DEBUG | Set to 1 to print stack traces on error. |

Roadmap

worm's substrate is already agent-agnostic — the warm pool, shared-file tunnels, lifecycle hooks, and your instructions file (CLAUDE.md / AGENTS.md / …) don't care which agent you run. The recipe engine is the part still wired specifically to Claude Code. The next two iterations open both up:

  • Shareable recipe packages — author a recipe in its own folder (~/.worm/recipes/<name>/) or a git repo and drop it in, instead of forking wormhole. A recipe becomes a directory of data + real script files, loaded alongside the built-ins, with its config validated from a manifest. Details: docs/recipes-roadmap.md §1–§2 and the "Next up" plan.
  • Agents beyond Claude — an agent-adapter seam so recipes can wire Cursor, Gemini, Codex, … not just Claude Code. Agents with a pre-tool/session hook system get full recipe support (sandbox, permission sync); agents without one still get the pool, tunnels, and shared instructions. Details: docs/multi-agent-roadmap.md.

The two halves fit together: a recipe-as-data format is what an agent adapter renders per agent. Earlier iterations — live-once recipe code, the inverted hook dispatcher, named stores, home-scope worm sync --global, and worm template render — are already shipped (the roadmap docs carry the full history). The one deferred item is a Terraform-style plan/apply for regenerating scaffolding you've edited.

Changelog

See CHANGELOG.md for the release history and any breaking changes (the current release, 0.2.0, has a few — notably the removal of the worm config command and a hook-dispatch migration). worm follows Semantic Versioning.

Development

pnpm install
pnpm typecheck
pnpm build
pnpm test           # runs node:test against the built CLI
pnpm demo           # visual walkthrough in an isolated sandbox

See src/README.md for architecture and CLAUDE.md for agent contributor guidelines.