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

termgate

v1.3.0

Published

Expose real shell sessions (bash/wsl/powershell) over a token-gated local HTTP + WebSocket API, with workspace confinement, a command risk policy, audit logging, and an MCP connector

Readme

termgate

Runs multiple real shell sessions (bash / wsl.exe / powershell.exe) on your machine, each controllable two ways:

  • REST API (/api/sessions/...) — list, create, run a command, read output, kill. Plain fetch() calls, no WebSocket client needed. This is the part a browser-based AI agent uses.
  • WebSocket (/ws/:id) — live interactive stream, used by the built-in browser terminal UI (xterm.js) so a human can watch/type too.

1. Install

npm install

node-pty compiles a native module.

  • WSL/Linux: sudo apt-get install -y build-essential python3
  • Windows native: install "Desktop development with C++" (VS Build Tools), then retry.

2. Run

npm start

Output:

UI:      http://127.0.0.1:4123/?token=<random-hex-token>
API:     http://127.0.0.1:4123/api/sessions   (header: Authorization: Bearer <token>)

One session named main is created automatically on startup.

3. Use from a Chrome-based AI agent

Point it at http://127.0.0.1:4123/?token=<token> to see/drive the terminal UI directly, or have it call the REST API with plain fetch:

const BASE = 'http://127.0.0.1:4123';
const TOKEN = '<your-token>';
const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${TOKEN}` };

// List terminals
const sessions = await fetch(`${BASE}/api/sessions`, { headers }).then(r => r.json());

// Create a new terminal
const s = await fetch(`${BASE}/api/sessions`, {
  method: 'POST', headers, body: JSON.stringify({ name: 'build' })
}).then(r => r.json());
// s.id -> use below

// Run a command on it and get the output it produced
const result = await fetch(`${BASE}/api/sessions/${s.id}/run`, {
  method: 'POST', headers, body: JSON.stringify({ command: 'npm run build', waitMs: 3000 })
}).then(r => r.json());
console.log(result.output);

// Poll for more output later (e.g. long-running command)
const more = await fetch(`${BASE}/api/sessions/${s.id}/output?since=${result.bufferLength}&token=${TOKEN}`)
  .then(r => r.json());

// Kill it when done
await fetch(`${BASE}/api/sessions/${s.id}`, { method: 'DELETE', headers });

CORS is open (Access-Control-Allow-Origin: *) so this works from any tab — the token is the actual gate, not the origin.

API reference

All endpoints require the token, either as Authorization: Bearer <token> header or ?token=<token> query param.

| Method | Path | Body | Purpose | |---|---|---|---| | GET | /api/sessions | — | List all sessions | | POST | /api/sessions | {name?, shell?, cwd?, cols?, rows?} | Create a session | | POST | /api/sessions/:id/run | {command, waitMs?} | Write command + newline, wait waitMs (default 1200, max 10000), return output produced in that window | | POST | /api/sessions/:id/input | {data} | Send raw keystrokes, no auto newline (for Ctrl+C, interactive prompts, etc.) | | GET | /api/sessions/:id/output?since=N | — | Full scrollback, or only what's new since char offset N | | POST | /api/sessions/:id/resize | {cols, rows} | Resize the pty | | DELETE | /api/sessions/:id | — | Kill the session |

WebSocket: ws://127.0.0.1:4123/ws/:id?token=<token> — sends {"type":"data","data":"..."} frames, accepts {"type":"input","data":"..."} and {"type":"resize","cols":N,"rows":N}.

Public access (--public)

For a remote or cloud-based agent (not local Chrome) that needs to reach your terminal, --public spins up a Cloudflare Quick Tunnel automatically and prints the public HTTPS URL alongside your token.

termgate --public

Requires cloudflared. termgate now installs it automatically if it's missing — on first --public run it tries the native package manager for your OS, and falls back to downloading the official binary directly if that's unavailable:

| OS | Tries first | Falls back to | |---|---|---| | Windows | winget install --id Cloudflare.cloudflared | direct .exe download to ~/.termgate/bin | | macOS | brew install cloudflared | direct .tgz download + tar extract to ~/.termgate/bin | | Linux | — (no sudo/package manager assumed) | direct binary download to ~/.termgate/bin |

No manual install step needed in the common case. If both the package manager and the download fail (e.g. no internet, blocked GitHub access), it prints manual install instructions and the server keeps running locally.

By default --public waits 5 seconds before starting the tunnel so you can Ctrl+C out if you change your mind. Skip that pause with --public --yes.

The server itself still only binds to 127.0.0.1cloudflared tunnels to that local port, it doesn't change what the Node process binds to. The public URL is randomly generated per run (https://xxxx.trycloudflare.com) and dies when you stop the tunnel — there's no fixed address to leak long-term, but the token in that session is still live for as long as the tunnel is up.

Treat --public as "anyone on the internet with this one URL+token gets a shell on my machine" — because that's exactly what it is. Concretely:

  • Don't leave it running unattended. Stop it (Ctrl+C) the moment you're done with whatever remote agent needed it.
  • Set a fixed, intentional token via TERM_TOKEN= before using --public rather than relying on the random one — makes it easier to reason about who currently has access, and to rotate cleanly by restarting.
  • There's no rate limiting or command allowlist here. If you want that layer, it needs to be added on top (happy to build it — see the note in the termgate skill file).

Remote MCP connector (add termgate directly to claude.ai)

termgate exposes a proper MCP endpoint at /mcp (Streamable HTTP, stateless) with tools: list_sessions, create_session, run_command, send_input, read_output, kill_session. This lets you add it as a custom connector in claude.ai itself, not just via Claude in Chrome browsing a webpage.

Setup:

  1. Run termgate --public (or --public --yes) to get a public HTTPS URL.
  2. In claude.ai: Settings → Customize → Connectors → Add → Add custom connector.
  3. URL: https://<your-tunnel>.trycloudflare.com/mcp
  4. Click Advanced settings and add a request header: Authorization: Bearer <your-token>
  5. Save, then enable it for a conversation via the + → Connectors menu in chat.

Read this before enabling it: claude.ai calls MCP connectors from Anthropic's cloud infrastructure, not from your device — so once this is added, this chat product itself can call real shell tools on your machine through the tunnel, from any device you're logged into. That's a step up from the Chrome-only path: no browser tab has to be open, and the connector stays configured until you remove it. Treat adding it the same way you'd treat handing out the token — because that's exactly what it is. Remove the connector (Settings → Connectors → remove) when you're not actively using it, and restart termgate to rotate the token between sessions.

Security model

Read this before pointing an agent (or the public tunnel) at termgate.

What's enforced (structured command path — REST /run, MCP run_command, REST /input, MCP send_input):

  • Workspace confinement. New sessions spawn in the folder you launched termgate from (the "launch folder"), or a subfolder of it — cwd outside that is rejected. Override the root with termgate --workspace /some/path.
  • Hard blocks. Known-destructive commands (rm -rf /, disk format, registry edits, fork bombs, disabling Defender/firewall, etc.) are refused outright — they never reach the shell.
  • Confirm gate. Risky-but-not-catastrophic commands (sudo, forced git operations, reading SSH/AWS credential files, download-and-execute pipelines, path traversal via ../) are held and return requiresConfirmation: true instead of running — re-send with confirm: true to actually execute them.
  • Every decision is audit-logged — see below.

What's NOT enforced — audit-logged only (interactive typing path — the browser terminal UI, raw WebSocket input messages): Blocking a live interactive shell mid-command would mean withholding every keystroke from the pty until Enter, which breaks live echo, backspace, tab-completion, Ctrl+C, and any raw-mode program (vim, ssh, password prompts). That's a fundamentally different, much harder engineering problem than gating a structured "run this one command" call. So: interactive typing is reconstructed into lines and logged with a risk label, but never blocked. If someone has the token and a terminal tab open, they can type anything a normal shell would accept.

This is pattern matching, not a sandbox. All of the above is text analysis on the command string — it can be bypassed with obfuscation (base64-encoded payloads, alternate interpreters, unusual quoting, multi-step indirection). It raises the bar against obviously destructive or careless commands and gives you a paper trail; it does not provide real isolation. For actual containment you'd need to run sessions inside a container or OS-level sandbox (Docker, a chroot/bwrap jail on Linux, Windows Sandbox) — a materially bigger project, since it also needs to decide what tools (git, npm, python, docker itself) are available inside that boundary. Worth doing as a follow-up if you need real isolation rather than a speed bump; ask and it can be scoped separately.

Audit log: every REST/MCP command (executed, blocked, or held) and every reconstructed interactive line is written to ~/.termgate/audit/YYYY-MM-DD.jsonl, one file per day, kept for 30 days (older files are deleted automatically on startup and once a day). View it via the Audit Log tab in the browser UI, GET /api/audit, or the MCP read_audit_log tool.

Config (env vars)

| Var | Default | Purpose | |---|---|---| | PORT | 4123 | Port to listen on | | HOST | 127.0.0.1 | Bind address — keep as localhost, see security note | | TERM_TOKEN | random on each start | Set a fixed token instead of a random one | | SHELL_CMD | wsl.exe on Windows, $SHELL on Linux/WSL | Default shell for new sessions (override per-session via POST /api/sessions body too) |

CLI flags: --public (start a Cloudflare tunnel after boot), --yes (skip the 5s confirmation pause when combined with --public), --workspace <path> (confine sessions to a specific folder instead of the current directory).

Fixed token example:

TERM_TOKEN=my-fixed-token SHELL_CMD=/bin/bash npm start

Claude Code skill

skills/termgate/SKILL.md documents the API for Claude Code so you can say /connect localhost:4123 (plus the token) and have it drive sessions directly. Copy that folder's contents into your project's .claude/skills/ (or wherever your Claude Code skill directory is set up) to enable it.

Read the "Important operating rules" section in that file before enabling it. Commands run this way execute immediately with no per-command confirmation — the token is the only gate, granted once at connect time, not per action.

Publishing to npm

  1. Rotate/generate a fresh npm access token — never reuse a token you've pasted anywhere in a chat or logged, treat any pasted token as compromised immediately.
  2. From this folder:
    npm login          # or: npm config set //registry.npmjs.org/:_authToken=<fresh-token>
    npm publish --access public
  3. Confirm the name termgate is still free right before publishing (npm view termgate should 404) — someone else could take it in the meantime.

Security notes — read before using

  • Whoever has the token gets a real, arbitrary shell on your machine. There's no user/permission model beyond the single token — every session runs as you.
  • Keep HOST=127.0.0.1. Don't bind 0.0.0.0 or forward the port to the internet as-is — this token check is a basic gate, not hardened auth (no rate limiting, no HTTPS, no per-session scoping).
  • CORS is wide open by design (so a browser agent tab can call it) — that means any page you have open could also call it if it somehow learned the token. Don't paste the token into untrusted pages or logs.
  • For remote/cloud-based agents (not local Chrome), tunnel instead of opening the port: cloudflared tunnel --url http://127.0.0.1:4123 or ngrok http 4123, then use the HTTPS tunnel URL + token. Rotate the token after.
  • Kill sessions (or the whole process, Ctrl+C) when you're done. Every open session is a live shell for as long as it exists.