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

claude-tmux-runner

v0.1.4

Published

Drive Claude Code interactively through tmux. A programmatic, deterministic replacement for `claude -p` that stays on subscription-window billing.

Downloads

744

Readme

claude-tmux-runner

Drive Claude Code interactively through tmux. A programmatic, deterministic replacement for claude -p that stays on subscription-window billing.

Motivation

On 2026-06-15, Anthropic is moving claude -p and the Agent SDK off subscription-window billing onto a separate metered monthly credit (Max 5x: $100, Max 20x: $200, doesn't roll over). Interactive claude (no -p flag) is explicitly excluded from that change and stays on regular plan quota.

If you've been driving Claude Code programmatically from a daemon (cron jobs, agent orchestrators, CI/CD), claude -p was the obvious choice. After June 15 it's expensive. This package replaces it with interactive claude spawned inside a detached tmux session — same programmatic interface, same N-parallel determinism, billed against your subscription window like any human-driven Claude Code session.

How it works

Each run() call:

  1. Generates or reuses a session UUID
  2. Injects a hook settings JSON that registers SessionStart + Stop hooks on the spawned claude process (used for deterministic TUI-ready and end-of-turn signals)
  3. Spawns a detached tmux new-session -d running interactive claude --session-id <uuid> (or --resume <uuid>)
  4. Waits for the SessionStart hook to fire (or for the TUI's chevron, if the hook harness couldn't be created)
  5. Pastes the prompt via tmux load-buffer + paste-buffer (binary-safe)
  6. Presses Enter; retries up to 4 times if the paste-render race eats the keystroke
  7. Tails the per-session JSONL at ~/.claude/projects/<encoded-cwd>/<uuid>.jsonl
  8. Waits for the Stop hook (or for an assistant entry with stop_reason: 'end_turn' on the fallback path)
  9. Reads token usage / terminal reason from the JSONL (using the Stop hook's transcript_path when available)
  10. Kills the tmux session
  11. Returns a RunResult

N concurrent run() calls don't interfere — each gets its own tmux session, its own JSONL path, and its own offset-aware tail.

The hook path is on by default; there's no opt-in flag. If the hook harness can't be created (unwritable tmp dir, etc.), the runner logs a warning and falls back to a screen-scrape EOT path ( chevron + paste-render detection + JSONL end_turn watch) that works without hooks.

CLI

The package ships a claude-tmux binary that mirrors claude -p so it can be dropped into any script that currently calls claude -p "...".

Install globally

npm install -g claude-tmux-runner
# or, per-project and invoke via npx:
npm install claude-tmux-runner

Usage

claude-tmux [options] [prompt]

| flag | description | |---|---| | -p, --prompt <text> | Prompt text (alternative to positional argument) | | --output-format <fmt> | text (default), json, or stream-json | | --model <id> | Model override, e.g. claude-opus-4-7 | | --system-prompt <text> | Append a system prompt (new sessions only) | | --effort <level> | low, medium, high, xhigh, or max | | --mcp-config <path> | Path to an MCP config JSON file | | --resume <session-id> | Continue an existing session | | --cwd <path> | Working directory for the claude process | | --no-progress-timeout <ms> | Max ms without output before timeout (0 disables) | | --no-pre-trust | Skip workspace-trust pre-population | | --claude-bin <path> | Path to the claude binary (default: claude) |

Exit codes

| code | meaning | |---|---| | 0 | Success (end_turn) | | 1 | Run failed (non-end_turn terminal reason) | | 2 | Bad args or missing dependency (tmux, claude) | | 124 | No-progress timeout | | 130 | Interrupted (SIGINT) |

Examples

# Basic text output
claude-tmux "What is 2+2?"

# JSON output (includes sessionId, durationMs, token counts)
claude-tmux --output-format json "Summarize this file" < notes.txt

# Stream events as NDJSON, then a run-result sentinel
claude-tmux --output-format stream-json --model claude-opus-4-7 "Explain tmux"

# Continue a prior session
SESSION=$(claude-tmux --output-format json "Start a story" | jq -r .sessionId)
claude-tmux --resume "$SESSION" "Continue the story"

# Pipe from stdin
echo "List three colors" | claude-tmux --output-format text

Install

npm install claude-tmux-runner

System requirements: macOS or Linux, tmux installed and on PATH, the claude binary on PATH (or specify claudeBin in the constructor).

Quickstart

import { ClaudeRunner } from 'claude-tmux-runner';

const runner = new ClaudeRunner({ cwd: process.cwd() });

// New session
const first = await runner.run({
  prompt: 'Who are you?',
  model: 'claude-opus-4-7',
  systemPrompt: 'You are a helpful assistant named Alfred.',
});
console.log(first.result);          // "I'm Alfred, ..."
console.log(first.sessionId);        // UUID — use this to continue the conversation

// Resume the same session for a follow-up
const second = await runner.run({
  prompt: 'What did I just ask you?',
  resumeSessionId: first.sessionId,
});
console.log(second.result);          // "You asked who I am."

API

new ClaudeRunner(options)

| option | type | default | description | |---|---|---|---| | cwd | string | (required) | Working directory the spawned claude runs in. Determines the JSONL path. | | claudeBin | string | 'claude' | Path to the claude binary. | | tmuxPrefix | string | 'claude-runner-' | Prefix for tmux session names. | | logger | Logger | no-op | Custom logger ({ info, warn, debug, error }). | | orphanGcMinAgeSec | number | 30 | Minimum age for gcOrphans() to kill a stale session. | | preTrustCwd | boolean | true | Pre-populate workspace-trust in ~/.claude.json so the TUI doesn't block on the trust dialog. |

runner.run(options): Promise<RunResult>

| option | type | default | description | |---|---|---|---| | prompt | string | (required) | Text pasted into the TUI. | | resumeSessionId | string | — | If set, continues an existing session. Otherwise creates a new one. | | model | string | (claude default) | e.g. 'claude-opus-4-7'. | | systemPrompt | string | — | Appended via --append-system-prompt. Only honored on new sessions. | | mcpConfigPath | string | — | Path to an MCP config JSON. | | effortLevel | 'low' \| 'medium' \| 'high' \| 'xhigh' \| 'max' | — | Passed as --effort. | | extraArgs | string[] | — | Extra CLI flags appended verbatim. | | noProgressTimeoutMs | number | 60_000 (or 1_200_000 when hook harness creation fails) | Max time without a new JSONL entry before bailing. The hook path's deterministic Stop signal makes a 60s watchdog right; the screen-scrape fallback path bumps to 20min for very long Claude turns that quietly digest large tool outputs. 0 disables. | | onStream | (event) => void | — | Streaming callback fired for each ConversationEvent. Meta entries are filtered before this fires. | | env | Record<string, string> | — | Extra env vars merged into the spawned process. |

RunResult

{
  sessionId: string;        // claude session UUID — pass to resumeSessionId for next turn
  result: string;           // concatenated assistant text from THIS turn only
  durationMs: number;
  tokensIn?: number;
  tokensOut?: number;
  tokensCacheRead?: number;
  tokensCacheWrite?: number;
  firstTurnPrefixTokens?: number;
  terminalReason?: 'end_turn' | 'tool_use' | 'max_tokens' | 'stop_sequence' | 'interrupted' | 'no_progress_timeout' | 'fatal_api_error' | string;
  apiErrorStatus?: number;  // HTTP status if a terminal api_error was observed
  error?: string;           // set on non-end_turn termination
}

runner.kill(sessionId): Promise<void>

Cancels an in-flight run() for that session UUID. Tears down the tmux session and resolves the pending run() promise with an interrupted result.

runner.killAll(): Promise<string[]>

Kill every tmux session this runner owns. Returns the names killed. Useful in shutdown handlers.

runner.gcOrphans(): Promise<string[]>

Boot-time GC for tmux sessions left by a crashed prior process. Age guard (orphanGcMinAgeSec) prevents killing freshly-spawned in-flight sessions.

Lower-level exports

For consumers that need finer-grained control (subscribing to JSONL events without spawning, normalizing JSONL externally, building custom orchestration):

  • EngineJsonlTail — per-file tail with isMeta filter, status tracking, api_error detection
  • JsonlStatusTracker, determineStatus, extractAssistantUsage — JSONL lifecycle helpers (MIT-lifted from ataraxy-labs/opensessions)
  • normalizeLine, parseBuffer — pure JSONL → ConversationEvent normalizer
  • ensureCwdTrusted — write ~/.claude.json trust flag
  • buildWrapperScript — zsh wrapper that sources rc files then execs the target
  • buildInteractiveClaudeArgsclaude CLI argv builder
  • newSession, sendKeys, sendText, capturePane, waitForPane, killAllRunnerSessions — low-level tmux helpers

See src/index.ts for the full re-export list.

Limitations / gotchas

  • macOS + Linux only. Windows isn't supported because tmux isn't a meaningful primitive there. WSL works.
  • tmux must be installed. This package shells out to tmux; without it, every run() call rejects.
  • Resume re-uses the prior session's system prompt. claude --resume <uuid> doesn't accept --append-system-prompt. Set the system prompt on the first turn; subsequent turns inherit it.
  • Workspace trust is best-effort. Failure to write ~/.claude.json is logged but doesn't abort the spawn. If the file is permission-locked, the first interactive run in a new cwd will hit the trust dialog and time out.
  • onStream filters meta entries. Claude Code occasionally injects isMeta:true entries (e.g. "Continue from where you left off." on certain resume paths) that the TUI hides. We filter them at the normalize layer so they don't pollute your stream.
  • PATH / rc files. When spawned from launchd / systemd / cron, the inherited PATH may not include claude. The default wrapper sources ~/.zprofile / ~/.zshenv / ~/.zshrc before exec. Override via buildWrapperScript's rcFiles option if you use bash or a different shell.

Testing

npm install
npm test              # vitest unit tests
bash test/cli-integration.sh  # end-to-end CLI smoke tests (requires tmux)

The unit test suite covers:

  • JSONL normalization (including isMeta filtering)
  • The per-file tail (offset handling, debounced drain, lifecycle status, api_error fail-fast)
  • The opensessions-lifted status state machine
  • Workspace-trust file writes
  • Post-run summary extraction
  • CLI arg parsing, validation, output formats, and exit codes

The integration test (test/cli-integration.sh) drives the full claude-tmux binary end-to-end with a mock claude binary, verifying the tmux session lifecycle, JSONL tail, and output formatting without a live Anthropic account.

Releasing

Releases are tag-driven. A push of vX.Y.Z to origin triggers the publish workflow at .github/workflows/publish.yml which calls into scripts/publish.sh do-publish for the actual build/test/npm publish steps. CI and local releases use the same script and the same auth path (an npm Granular Access Token, referenced as ${WKOUTRE_NPM_TOKEN} in the project's .npmrc), so behaviour stays in sync.

Standard release (recommended)

npm run release patch    # 0.1.1 -> 0.1.2
npm run release minor    # 0.1.1 -> 0.2.0
npm run release 0.2.0    # explicit

(Equivalent to ./scripts/publish.sh release <bump>.)

The script bumps package.json, runs build + tests, commits release: vX.Y.Z, creates an annotated tag, and pushes the branch + tag. CI handles the npm publish from there.

Direct publish (CI internals, or local emergency)

npm run do-publish

(Equivalent to ./scripts/publish.sh do-publish.)

Validates package.json matches the v* tag at HEAD, then runs clean + build + test + npm publish. CI and local both substitute ${WKOUTRE_NPM_TOKEN} into the project's .npmrc at runtime; in CI the workflow maps the NPM_TOKEN repo secret to that env var. No interactive 2FA prompt because the token has the bypass flag set.

One-time setup

The token: generate an npm Granular Access Token at https://www.npmjs.com/settings/<your-user>/tokens. Click "Generate New Token" → "Granular Access Token". Configure:

  • Token name: NPM_TOKEN
  • Bypass two-factor authentication (2FA): CHECK this box. Required for non-interactive publishes; without it npm prompts for an OTP and the workflow fails with EOTP.
  • Allowed IP ranges: leave blank.
  • Packages and scopes / Permissions: "Read and write". Scope to claude-tmux-runner only.
  • Organizations / Permissions: "No access".
  • Expiration: 90 days is the maximum for read/write tokens. Plan a rotation reminder.

Copy the token immediately (npm shows it once).

For CI: add to GitHub at https://github.com/wkoutre/claude-tmux-runner/settings/secrets/actions as a repository secret named NPM_TOKEN. The publish workflow maps it to WKOUTRE_NPM_TOKEN in the job env so it matches what .npmrc references.

For local: export it from your shell config (e.g. ~/.zsh/conf.d/99-local.zsh):

export WKOUTRE_NPM_TOKEN=npm_xxxx...

Open a new shell or source the file. The project's .npmrc reads ${WKOUTRE_NPM_TOKEN} at runtime so npm publish from this checkout will use the token. npm install is unaffected (the auth line only applies to operations that require auth, which install of public packages doesn't).

The WKOUTRE_ prefix is intentional: it keeps the var clear of other npm-publishing tooling (lerna, semantic-release, release-please) that defaults to the generic NPM_TOKEN name.

License

MIT. Includes MIT-lifted code from ataraxy-labs/opensessions (see LICENSE for the attribution).