hausfold-scruff (Python SDK)
A thin Python client over the scruff binary — the
worktree-lifecycle substrate for parallel coding agents. scruff has no daemon,
so this SDK shells out to it (asyncio.create_subprocess_exec + --json,
watch --json for a live NDJSON stream).
Async-first: watch() is naturally a stream. A sync script can still call
every method via asyncio.run(...).
Import name is scruff; the package on PyPI is hausfold-scruff.
Install
pip install hausfold-scruff
# or: uv add hausfold-scruff
For local development against this repo instead: pip install -e sdk/python.
scruff itself must be on PATH, or pass ScruffClientOptions(bin="/path/to/scruff").
Two shapes of usage
Programmatic. Every ScruffClient method except the two ending in
_interactive captures the child's stdout and returns — safe to call from
a server with many concurrent sessions.
import asyncio
from scruff import ScruffClient
async def main() -> None:
scruff = ScruffClient()
envelope = await scruff.list()
for lane in envelope.lanes:
# occupied/dirty are `bool | None`: None means "not determined",
# never coerce it to False.
print(lane.name, lane.state, lane.occupied)
# Create a lane WITHOUT attaching an agent to it — the primitive an
# orchestrator wants. child/spawn only ever print the new path.
lane_dir = await scruff.child("/path/to/some-repo", "task-42")
# ...now launch YOUR OWN agent process against lane_dir.
asyncio.run(main())
# Live updates instead of polling — created/parked/resumed/reaped/changed.
async for line in scruff.watch():
if line.kind == "created" and line.lane is not None:
notify_ui(line.lane)
# Or scoped to the one lane this session holds — no hello/ready framing,
# and nothing about anybody else's lanes.
async for event in scruff.watch_lane(lane_dir):
if event.kind == "reaped":
end_session()
Interactive. new_interactive / resume_interactive inherit the
calling process's stdio, so when scruff execs the configured agent client
(claude, codex, opencode, pi) it takes over the real terminal — same as
running scruff new by hand — and control returns to you when that session
ends.
# A terminal app, run in an actual TTY:
await scruff.new_interactive("task-42")
# ... the agent owned the screen; you're back here when it exits.
Do not call new_interactive from a server — scruff new execs the
agent client unconditionally, without checking for a TTY, so piped stdio
blocks forever. Use resume() instead: it detects piped stdout and prints
the reopen command as text rather than exec'ing.
Holding a session open: leases
scruff's sweep (reap) needs to know a checkout is in use. On a human's
machine, lsof answers that; a server has no pane or shell cwd'd anywhere,
so it says so itself with a lease:
lease = await scruff.lease(lane_dir) # refreshes on an interval, < the 90s TTL
# ... serve the session ...
await lease.release()
Pass pid= instead when the lease should track a real local process — the
OS then drops it the instant that pid dies, no refresh loop needed.
A lease can only save a lane from reap, never condemn one — "nobody
leased it" isn't proof nobody's there.
scruff.lease(...) is a coroutine, so it can await the first heartbeat before
returning: a failure to take the lease raises immediately instead of
surfacing on the next refresh or release call.
watch() cleanup
watch() returns an async generator; stop consuming (break, or
.aclose()) to kill the underlying process. Async generators aren't
guaranteed to close promptly when they go out of scope — wrap long-lived use
in contextlib.aclosing() to tear down the subprocess deterministically:
from contextlib import aclosing
async with aclosing(scruff.watch()) as stream:
async for line in stream:
...
Types for a frontend
scruff.types has no runtime dependencies beyond the standard library.
Import just the dataclasses if you're modeling the same wire shape
elsewhere:
from scruff import ScruffLane, WatchEvent
What's NOT here yet
hook create/hook remove have no wrapper — shell out via run() if you
need them. Types are hand-ported from the Go structs, not generated; if
scruff's JSON shape drifts from this file, that's a bug here.
Testing
tests/fake-scruff.sh stands in for the real binary so tests don't need a Go
build.
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest
mypy src
Release files for hausfold-scruff 1.3.7
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| hausfold_scruff-1.3.7.tar.gz | 14.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| hausfold_scruff-1.3.7-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 30.1 kB
Release files / hausfold_scruff-1.3.7.tar.gz
| Download URL | hausfold_scruff-1.3.7.tar.gz |
|---|---|
| Size | 14.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e65720f527dddd86a95e621d9f15a4a9aab91a45e991a3a28c2b33f348cf2523
|
|
BLAKE2b-256 checksum How to use checksums |
2b2cd2d8d5f30ce7bb1133de246f9dc83959c317f91565341ccb81ef8fb27f29
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.
Transparency logRelease files / hausfold_scruff-1.3.7-py3-none-any.whl
| Download URL | hausfold_scruff-1.3.7-py3-none-any.whl |
|---|---|
| Size | 15.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
271b89844ec64420acfff2b74c2aa0e8522ddb969642e70a19e083f96cb791fa
|
|
BLAKE2b-256 checksum How to use checksums |
b87324374a626814de6e8409bc88a583cf0760fd4b88c3bf2eaf3c47efb61030
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.
Transparency log