Skip to main content

Architecturally-capped pacing for parallel claude -p calls. Automate yourself, not around Anthropic.

Project description

pinch

Automate yourself, not around Anthropic. Architecturally-capped pacing for parallel claude -p calls.

A tiny library (Node + Python) you drop into your own automation. It holds the number of parallel claude invocations, the interval between them, and the daily schedule below the pattern a single human power-user could plausibly sustain — and it refuses to be configured any other way.

License: MIT Ethics


Why this exists

In early 2026 Anthropic clarified that Claude Code Pro/Max subscriptions are for "ordinary, individual usage" and that OAuth tokens from those subscriptions must not be plugged into third-party tools, self-built orchestrators, or the Agent SDK. The community reacted two ways:

  1. Proxy servers that rewrap tokens to evade the policy — dishonest and against the spirit of the terms.
  2. Migration to API-key billing — fair, but prohibitively expensive for one-person projects.

pinch is a third path. A solo developer legitimately opens 5–6 terminals running claude, juggles 2–3 projects, and takes nights off. If your automation never exceeds any of those dimensions, it is architecturally indistinguishable from you using the tool by hand — both in letter and in spirit. That's what pinch enforces.

See ETHICS.md for the compliance rationale.


The invariants you cannot override

These are hard-coded. The config validator throws InvariantViolation on startup if any of these would be relaxed. There is no flag, no env var, no monkey-patch hook. To change them you'd have to fork the project under a different name.

Invariant Cap Rationale
max_global_parallel_sessions ≤ 5 Physical ceiling for a live power-user
max_parallel_per_project ≤ 3 Plan + dev + review = realistic max
max_active_projects ≤ 3 Human context-switching limit
min_downtime_hours_per_day ≥ 8 Sleep. Non-negotiable
max_working_hours_per_day ≤ 16 Derived from downtime
min_spawn_delay_ms ≥ 15 000 Humans read, think, switch windows
min_wave_cooldown_ms ≥ 120 000 Pause between batches of starts

Everything else — exact delays, window times, wave size, timezones, runner args — is configurable within these bounds.


Install

# Node
npm install itsalt-pinch

# Python
pip install itsalt-pinch

The package is distributed as itsalt-pinch on both registries (the short pinch name was already taken on npm and PyPI). The import name is still pinch:

import { Pacer } from "itsalt-pinch";
from pinch import Pacer   # yes, underscore-less — the wheel
                          # installs into the `pinch` package

Zero runtime dependencies on Node. On Python the only dep is tzdata (pure-data package) and only on Windows, because Windows lacks the IANA timezone database in stdlib.


Quick start — Node

import { Pacer } from "itsalt-pinch";

const pacer = new Pacer({
  workingWindow: { start: "08:00", end: "23:00", tz: "Europe/Moscow" },
  hooks: {
    onStarted:  (e) => console.log("▶", e.taskId, e.projectId),
    onFinished: (e) => console.log("✓", e.taskId, e.exitCode, `${e.durationMs}ms`),
    onBlocked:  (e) => console.log("⏸", e.reason, e.msUntilRetry),
  },
});

// Single task
const result = await pacer.run({
  prompt:    "refactor this file for readability",
  projectId: "api-gateway",
  cwd:       "/home/me/api-gateway",
});

// Batch
const results = await pacer.runBatch([
  { prompt: "write tests", projectId: "api-gateway" },
  { prompt: "add docstrings", projectId: "api-gateway" },
  { prompt: "review PR #42", projectId: "web-frontend" },
]);

await pacer.drain();
await pacer.shutdown();

Quick start — Python

import asyncio
from pinch import Pacer, Task, HookMap, PacerOptions

async def main():
    pacer = Pacer(PacerOptions(
        working_window={"start": "08:00", "end": "23:00", "tz": "Europe/Moscow"},
        hooks=HookMap(
            on_started=lambda e: print("▶", e["task_id"], e["project_id"]),
            on_finished=lambda e: print("✓", e["task_id"], e["exit_code"]),
            on_blocked=lambda e: print("⏸", e["reason"]),
        ),
    ))

    result = await pacer.run(Task(
        prompt="refactor this file for readability",
        project_id="api-gateway",
        cwd="/home/me/api-gateway",
    ))

    results = await pacer.run_batch([
        Task(prompt="write tests", project_id="api-gateway"),
        Task(prompt="add docstrings", project_id="api-gateway"),
        Task(prompt="review PR #42", project_id="web-frontend"),
    ])

    await pacer.drain()
    await pacer.shutdown()

asyncio.run(main())

Configuration reference

Defaults (also the values above the hard floor):

workingWindow:
  start: "08:00"
  end:   "23:00"
  tz:    "<system default>"
pacing:
  spawnDelayMs:   {min: 15000,  max: 30000}
  waveCooldownMs: {min: 120000, max: 300000}
  waveEveryN: 5
limits:
  maxGlobalParallelSessions: 5
  maxParallelPerProject:     3
  maxActiveProjects:         3
runner:
  claudeBinary:  "claude"
  taskTimeoutMs: 600000
  args: ["--print"]

All fields can also be set via environment variables (PINCH_*). Options passed to the Pacer constructor win over env vars, which win over the defaults. See docs/invariants.md for the floor of each numeric field.


Observability

pinch emits four events you can subscribe to via the hooks option:

  • onEnqueued — task accepted into the queue
  • onStarted — permit acquired, claude about to spawn
  • onFinishedclaude exited (successfully or not)
  • onBlocked — dispatcher is waiting; includes reason (window-closed, global-limit, project-limit, active-projects, wave-cooldown, upstream, overrun) and msUntilRetry

That's the only surface. Send these wherever you want — a log file, stdout, your Prometheus exporter, whatever. pinch doesn't ship its own metrics.


FAQ

Can I raise a limit? No, by design. If a flag could do it, the whole premise of this library collapses — anyone staring at a "violation" could just flip the flag. If you need more than 5 parallel sessions, you are beyond the individual-use envelope and should be on the API.

Two Pacer instances on one machine? pinch is single-process in v1; it does not know about other instances. Running two concurrently means you can get 2×5 = 10 parallel sessions, which blows the invariant. Don't do it. v1.1 will add cross-process coordination via a lockfile.

Does it work with subscription and API key? Yes, pinch is auth-agnostic. It spawns claude -p as a subprocess and doesn't care how that binary authenticates. Both groups benefit: subscribers from the behavioural ceiling; API users from not accidentally running up a bill.

Is this endorsed by Anthropic? No. This is an independent, good-faith interpretation of public terms. See ETHICS.md and use at your own risk.

Is OAuth token handling involved? No. pinch never touches tokens, never reads configs, never rewrites endpoints. It runs claude with arguments you provide — nothing more.


Related projects

  • NaCl — the broader business-/systems-analysis framework this library was carved out of.

Contributing

Issues and PRs welcome. Please read ETHICS.md and docs/invariants.md before proposing a change that touches the hard caps — they are the reason this project exists.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

itsalt_pinch-0.1.2.tar.gz (16.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

itsalt_pinch-0.1.2-py3-none-any.whl (20.5 kB view details)

Uploaded Python 3

File details

Details for the file itsalt_pinch-0.1.2.tar.gz.

File metadata

  • Download URL: itsalt_pinch-0.1.2.tar.gz
  • Upload date:
  • Size: 16.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for itsalt_pinch-0.1.2.tar.gz
Algorithm Hash digest
SHA256 b55033fbd5faf7676f2bcd7a629eac1fa6bf29df544fa508d5fa95164bf421e3
MD5 ed244fe6d49e4d7b8eaac0862581d703
BLAKE2b-256 a867d6d4efef8877661855d20131c97d3636119ee6c8ff238f6b3063d8f0cf3a

See more details on using hashes here.

Provenance

The following attestation bundles were made for itsalt_pinch-0.1.2.tar.gz:

Publisher: release.yml on ITSalt/pinch

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file itsalt_pinch-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: itsalt_pinch-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 20.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for itsalt_pinch-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 9fa1c02dcc0e3e0a5caf2f42f9e60d4f63498a8ebaf35b88343a439cfb14527d
MD5 3231a1ca430f6276b30e4c002474826b
BLAKE2b-256 925acec7e59dbb1a61fede9910ed8164d2bdcdc9cd90c6a783414f564d9f7d07

See more details on using hashes here.

Provenance

The following attestation bundles were made for itsalt_pinch-0.1.2-py3-none-any.whl:

Publisher: release.yml on ITSalt/pinch

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page