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 -pcalls.
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.
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:
- Proxy servers that rewrap tokens to evade the policy — dishonest and against the spirit of the terms.
- 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 queueonStarted— permit acquired,claudeabout to spawnonFinished—claudeexited (successfully or not)onBlocked— dispatcher is waiting; includesreason(window-closed,global-limit,project-limit,active-projects,wave-cooldown,upstream,overrun) andmsUntilRetry
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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8638d7babbaddc9fc08df1ec4b0712b425db62ebe86805b7a04feb7f179f58e7
|
|
| MD5 |
457bfa2c377445afde42ef63e1e1da3d
|
|
| BLAKE2b-256 |
21740cf940e6f3de175943dd0926ad32a16da52aa6733bf20476ff2907b49c58
|
Provenance
The following attestation bundles were made for itsalt_pinch-0.1.3.tar.gz:
Publisher:
release.yml on ITSalt/pinch
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
itsalt_pinch-0.1.3.tar.gz -
Subject digest:
8638d7babbaddc9fc08df1ec4b0712b425db62ebe86805b7a04feb7f179f58e7 - Sigstore transparency entry: 1338774791
- Sigstore integration time:
-
Permalink:
ITSalt/pinch@a5b1710d7c64242a56eddff3d5e01f8dba13fd66 -
Branch / Tag:
refs/tags/v0.1.3 - Owner: https://github.com/ITSalt
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a5b1710d7c64242a56eddff3d5e01f8dba13fd66 -
Trigger Event:
push
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f9efd89d23d6a517cd48bb667f41a54eb6e1886ae6c9db0b5210585c52a0dc0b
|
|
| MD5 |
ed5040e82983ea2203f50064a70b6482
|
|
| BLAKE2b-256 |
436148fd105f7f10d78a72e0155c5998a2f660dd6a8aef5475878ee479352995
|
Provenance
The following attestation bundles were made for itsalt_pinch-0.1.3-py3-none-any.whl:
Publisher:
release.yml on ITSalt/pinch
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
itsalt_pinch-0.1.3-py3-none-any.whl -
Subject digest:
f9efd89d23d6a517cd48bb667f41a54eb6e1886ae6c9db0b5210585c52a0dc0b - Sigstore transparency entry: 1338774794
- Sigstore integration time:
-
Permalink:
ITSalt/pinch@a5b1710d7c64242a56eddff3d5e01f8dba13fd66 -
Branch / Tag:
refs/tags/v0.1.3 - Owner: https://github.com/ITSalt
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a5b1710d7c64242a56eddff3d5e01f8dba13fd66 -
Trigger Event:
push
-
Statement type: