Skip to main content

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.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for hausfold-scruff 1.3.0
File Size Uploaded
hausfold_scruff-1.3.0.tar.gz 14.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for hausfold-scruff 1.3.0
File Interpreter ABI Platform
hausfold_scruff-1.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 29.7 kB

Release files / hausfold_scruff-1.3.0.tar.gz

Download URL hausfold_scruff-1.3.0.tar.gz
Size 14.1 kB
Tags Source
SHA-256 checksum
How to use checksums
73a47093f71bcbffc1d59c62b2d12cf53f0d2b02656d870c8f79386a595c8df4
BLAKE2b-256 checksum
How to use checksums
ed3e99042c8dbcb275f5c55ac43650cf3024acd6ef6f611a8fbffaabc6c54095
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 Aug 29, 2026.

Transparency log

Release files / hausfold_scruff-1.3.0-py3-none-any.whl

Download URL hausfold_scruff-1.3.0-py3-none-any.whl
Size 15.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ef0867caa1bb3a50ebb33a17f9e3ed001dcb7de55ee2c2e9824cbe564614a066
BLAKE2b-256 checksum
How to use checksums
1f9f2be509f03ac803aa0ca0b7b3e54bf9f2885912f04558440fc5deeb71c889
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 Aug 29, 2026.

Transparency log

Release history Release notifications | RSS feed

1.6.1

2 release files

1.6.0

2 release files

1.5.1

2 release files

1.5.0

2 release files

1.4.0

2 release files

1.3.7

2 release files

1.3.6

2 release files

1.3.5

2 release files

1.3.4

2 release files

1.3.3

2 release files

1.3.2

2 release files

1.3.1

2 release files

This release

1.3.0 This release

2 release files

1.2.0

2 release files

1.1.0

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page