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.3.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.3-py3-none-any.whl (20.5 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: itsalt_pinch-0.1.3.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.3.tar.gz
Algorithm Hash digest
SHA256 8638d7babbaddc9fc08df1ec4b0712b425db62ebe86805b7a04feb7f179f58e7
MD5 457bfa2c377445afde42ef63e1e1da3d
BLAKE2b-256 21740cf940e6f3de175943dd0926ad32a16da52aa6733bf20476ff2907b49c58

See more details on using hashes here.

Provenance

The following attestation bundles were made for itsalt_pinch-0.1.3.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.3-py3-none-any.whl.

File metadata

  • Download URL: itsalt_pinch-0.1.3-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.3-py3-none-any.whl
Algorithm Hash digest
SHA256 f9efd89d23d6a517cd48bb667f41a54eb6e1886ae6c9db0b5210585c52a0dc0b
MD5 ed5040e82983ea2203f50064a70b6482
BLAKE2b-256 436148fd105f7f10d78a72e0155c5998a2f660dd6a8aef5475878ee479352995

See more details on using hashes here.

Provenance

The following attestation bundles were made for itsalt_pinch-0.1.3-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