Skip to main content

headspace-cli

Headspace is an ephemeral computational workspace for agents. It lets a model offload execution, experimentation, and calculation into a bounded, isolated workspace, and gets back a compact, evidence-bearing result instead of a raw execution transcript.

The problem

Agent runtimes today externalize computation through a generic shell or an ad hoc sandbox, which conflates running a command with managing a workspace and gives the model little control over what re-enters its context. As docs/headspace_cli_issue_requirements.docx (§2) puts it:

Agent runtimes need a safe place to externalize computation without turning the model context into an execution log. Generic command runners can execute work, but they do not provide a task-level abstraction for temporary state, artifact retention, provenance, result compression, or selective return to the model.

That absence creates recurring, specific problems the same document names: execution output competes with requirements and reasoning for limited context space; temporary files and intermediate state have unclear ownership; the model must interpret raw logs even when only a conclusion or artifact matters; reproducibility and provenance vary by backend; and security, network access, and resource budgets are often left implicit rather than declared. Headspace is the abstraction that closes that gap: a workspace with an explicit lifecycle, a declared policy, and a result package that carries only what deserves to re-enter the model's context.

What's in this repository

  • The CLI (headspace on PATH once installed) — seven lifecycle verbs that create and use a workspace, plus six introspection verbs that describe this agent and its CLI surface. Every verb supports --json; every lifecycle verb supports --provider {docker,fake}. See CLI below.
  • headspace/core/ — the backend-neutral engine: a nine-state lifecycle (states.py), closed-by-default policy with an enforced-vs-measured effective view (policy.py), CLI-owned state under ~/.headspace with fail-closed schema versioning and per-workspace locking (store.py), digest-pinned runtime profiles (profiles.py), atomic digest-verified artifact export (artifacts.py), the mirror-image host-to-workspace copy-in that measures a digest before any provider is touched (inputs.py), the compact context-return result package (result.py), and orchestration with an intent journal and crash reconciliation (workspace.py).
  • headspace/providers/ — an eight-verb Provider protocol (capabilities, create, run, inspect, read, write, stop, remove), and two implementations that both pass the same conformance suite: an in-memory fake (no engine required — what makes the whole test suite runnable with nothing installed) and a real docker provider. docker>=7.1,<8 is the package's one runtime dependency.
  • A mesh identity — culture.yaml (suffix + backend) and the matching resident prompt file (AGENTS.colleague.md, since this agent runs backend: colleague).
  • The canonical guildmaster skill kit (18 skills) under .claude/skills/, vendored cite-don't-import. See docs/skill-sources.md.
  • A build + deploy baseline — pytest, lint, the agent-first rubric gate, and PyPI Trusted Publishing wired into GitHub Actions.

Quickstart

uv sync
uv run pytest -n auto                 # run the test suite; docker-dependent
                                       # tests skip cleanly with no engine
uv run headspace whoami               # identity from culture.yaml
uv run headspace learn                # self-teaching prompt (add --json)
uv run teken cli doctor . --strict    # the agent-first rubric gate CI runs

CLI

Lifecycle verbs

The seven verbs that create and use a workspace. Each returns the same nine-section result package and takes its process exit code from that package's status (see Exit codes).

Verb What it does
create Provision an ephemeral workspace under a declared policy; the only verb that mints a workspace id.
put <workspace> <host-path> <destination> Copy a host file or directory into a workspace, recording its destination, size and sha256 — never its bytes. Refuses an existing destination unless --overwrite.
run <workspace> <command> ... Execute one command in an existing workspace. Flags for run go before the workspace id — everything after it belongs to the command. --input, --env and --env-file get files and secrets into the job without argv (see below).
stop <workspace> End a workspace's in-flight job. Previews by default — reports what is running and changes nothing; --apply ends it, and the stop invocation's own result reads cancelled.
inspect <handle> Report a workspace's lifecycle state, the engine's view of it, and what the session has cost. --logs returns captured output in full and unbounded.
export <workspace> <name> --to <path> Publish a declared artifact to a durable host path, digest-verified, by atomic rename.
destroy <workspace> Remove a workspace — or refuse, and remove nothing, when declared artifacts were never exported (--force to discard them deliberately).

Getting files and secrets into a job

Two consumer-filed gaps, closed the same way: a way to get something in without smuggling it through argv, which is recorded verbatim in four places — outcome_summary, provenance.inputs, journal.jsonl and state.json.

Files (#14). headspace put <workspace> <host-path> <destination> [--overwrite] copies a host file or directory into a workspace, recording the destination, size and sha256 — never the bytes. run --input NAME=HOST_PATH (repeatable; NAME is the workspace-relative destination, HOST_PATH may be a file or a whole directory) drives the identical copy-in before the job starts — there is one implementation of getting bytes into a workspace, not two that agree until they don't. --input deliberately has no overwrite affordance: replacing an existing destination is a deliberate act that belongs to put, not something a job launch does implicitly.

Secrets (#13). run --env NAME (repeatable) reads the named variable out of the caller's own environment — the value never enters argv, so it never reaches any of the four recording surfaces above — and refuses an unset name rather than forwarding an empty string, and --env NAME=VALUE outright (a value typed into the flag is exactly the leak it exists to close). run --env-file PATH (repeatable) reads NAME=VALUE lines from a host file: blank lines and # comments are skipped, every other line must be an assignment with no quote stripping and no export prefix, and a malformed line is refused by naming the file and the 1-based line number — never by quoting the line back, since that line is exactly the kind that might hold a secret.

The recording guarantee, stated with its edge. Across put and both run flags: names, paths and digests are recorded; values and file contents never are. That guarantee covers what headspace itself composes into the result package, the journal and state.json — it does not cover a job that prints its own environment. A job that runs env, printenv, or hits a traceback that dumps os.environ writes the value into its own captured output, and captured output is kept, in the job's record in state.json and rendered as evidence. That is the job's doing, not headspace's, and no recording discipline on this side can unsee it — run --help says so where the flags live, and it is worth repeating here: a guarantee that overclaims is worse than one that states its edge.

There is a second edge, in the engine rather than in headspace. A forwarded value reaches the job the only way a process environment can be set, so while the job container exists the value is readable from the container's own configuration — docker inspect on that container shows it under Config.Env. Measured, not assumed. Two bounds hold and are tested: the value reaches neither the container's command nor its labels, and the job container is removed as soon as the job settles, so the exposure lasts the job and not the workspace. But the honest statement is that --env keeps a secret out of headspace's durable records, not out of the reach of whoever can already talk to your Docker daemon — and anyone who can do that could read the value out of a running process anyway. Choose --env because it beats argv, which is recorded forever in four places; not because it hides a value from the machine the job runs on.

Introspection verbs

Verb What it does
whoami Report this agent's nick, version, backend, and model from culture.yaml.
learn Print a structured self-teaching prompt.
explain <path> Markdown docs for any noun/verb path.
overview Read-only descriptive snapshot of the agent.
doctor Check the agent-identity invariants (prompt-file-present, backend-consistency).
cli overview Describe the CLI surface itself.

Every command supports --json. Results go to stdout, errors/diagnostics go to stderr, never mixed — which is what makes --json safe to pipe.

Worked example: create, run, export, destroy

This is a real, verified run against the docker provider (the default backend; pass --provider fake for an in-memory backend that needs no engine — see Choosing a backend).

$ uv run headspace create --workspace-id demo
# headspace result
## Outcome summary
workspace demo is ready on docker, running the python3.12 profile
## Status
success
## Key findings
- lifecycle state: ready
- network: disabled
- environment digest: sha256:57cd7c3a7a273101a6485ba99423ee568157882804b1124b4dd04266317710de
## Warnings and limitations
- the storage limit is measured, not enforced: reported by the engine, not capped (default local volume driver)
...

$ uv run headspace run --declare report.json="pi to 10 digits, with a sanity check" \
    demo python -c "
import json, math
value = round(math.pi, 10)
check = abs(value - 3.1415926536) < 1e-9
with open('report.json', 'w') as f:
    json.dump({'pi': value, 'check_passed': check}, f)
print(f'pi={value} check_passed={check}')
"
# headspace result
## Outcome summary
job job-9324b5e5a3cd ran python -c ... in workspace demo and reported success
## Status
success
## Evidence
- label: captured output
  - kind: excerpt
  - source: job-9324b5e5a3cd
  - truncated: false
  - excerpt:
    pi=3.1415926536 check_passed=True
## Suggested attention
- artifact 'report.json' is declared but not exported (pi to 10 digits, with
  a sanity check); export it, or a destroy will refuse until you discard it
  with force

$ uv run headspace export demo report.json --to ./report.json
# headspace result
## Outcome summary
artifact 'report.json' left workspace demo and is durable at ./report.json
## Key findings
- 42 bytes verified as sha256:e0c22d1940fa5e341a0e32cadee14260530645847d1b135c6df6087ad8a16aca
- the artifact now outlives its workspace

$ cat ./report.json
{"pi": 3.1415926536, "check_passed": true}

$ uv run headspace destroy demo
# headspace result
## Outcome summary
workspace demo was destroyed on docker
## Key findings
- removed: runtime, storage
- lifecycle path: cancelled -> destroyed

Every one of those four steps is a single, bounded, evidence-bearing result — the container's stdout, exit code, and resource usage never had to enter the caller's context directly; only the parts that mattered did. Output above is trimmed for length; the full result package also carries artifacts, resource_usage, provenance, and attention sections on every call.

The three consumer classes

Every verb's result package has exactly two renderings for three audiences:

  • AI agents and humans read the default markdown, walked from the same nine-section structure every time.
  • Scripts and non-AI bots read --json, the identical structure serialized instead of rendered.

Both come from one in-memory structure, so the two can never disagree about content — the CLI never renders text and parses it back. The default rendering is bounded to 8 KiB (--max-result-bytes to change it); whenever it drops bytes, it says so with a marker naming the exact retrieval command (headspace inspect <handle> --logs) rather than silently hiding the volume that didn't fit.

Choosing a backend

Every lifecycle verb takes --provider docker (the default, a real container per job) or --provider fake (an in-memory backend that passes the same conformance suite as Docker, so it's a genuine second implementation, not a stub). fake needs no engine, which is what makes the entire test suite runnable with nothing installed — but its workspaces live only in the CLI process's memory, so they do not survive between separate shell invocations the way docker's (state under ~/.headspace, reconciled against engine labels) do. Use fake from a single Python process (tests, a script driving the CLI's main() directly); use docker for a multi-step shell session like the one above.

Security posture

Every workspace starts closed by default: no network access, and a small, explicit resource budget — 512 MiB memory, 1 CPU core, 128 pids, 1 GiB storage, a 300-second wall clock, 10 MiB of captured output, 1 concurrent job — never "unlimited." On the docker provider that policy becomes a real container HostConfig, shared by the workspace container and every job container it starts: network_mode is none unless --network enabled was passed; mem_limit and memswap_limit are pinned equal (a container allowed to swap has not really been given a memory limit); nano_cpus and pids_limit come straight from the budget; privileged is always false with security_opt: [no-new-privileges]. The only mount is the workspace's own bounded volume — no host bind mounts, no devices, no volumes_from, and never the Docker engine socket itself. wall_clock_seconds, output_bytes, and concurrency are enforced by headspace's own process, independent of the engine.

Say plainly what that does and does not guarantee: every limit in a result package's policy summary is labelled enforced (the host will really apply it) or measured (the host only reports it). The one limit that is measured, not enforced, today is storage, on Docker's default local volume driver — the engine exposes a volume's size through its /system/df accounting, and reports it there, but no driver on offer takes a size cap. That gap is surfaced in every affected result's warnings, not hidden. If the host cannot really back a requested limit that does guard the isolation boundary (network, filesystem scope, memory, cpu, pids), create fails closed — exit 3, before anything runs — rather than silently serving a weaker box. The MVP Docker provider also supports no host-path mounts at all, so --allow-host-path fails closed on it today, by design, not by omission.

Exit codes

Code Category Meaning
0 success the verb completed.
1 user_error bad flag, missing argument, or unknown path — caller error.
2 environment_error a local setup/tooling problem.
3 policy_denied the declared policy could not be satisfied — refused before anything ran.
4 timeout a wall-clock or budget ceiling was hit.
5 cancelled the caller asked for it to stop — headspace stop <workspace> --apply ended a job in flight, and the stop invocation's own result reads cancelled. On the docker provider the job's separately recorded outcome doesn't yet follow: it still reads failure/6, a known gap tracked as #16.
6 computation_failed the job ran correctly and produced a failing result.
7 infrastructure_failure the engine or environment broke — not a computational failure.
8 resource_exhausted the job was killed for exceeding its declared memory ceiling.

0–2 are the original CLI-error codes and are unchanged. 3–8 are the failure taxonomy, mirroring the result package's status vocabulary one to one — a caller learns why a job failed from the exit code alone, without parsing text.

A command the image cannot execute at all is reported as 6 (computation_failed), not 7 — it is the caller's mistake, not a broken engine, and retrying it unchanged will not help. The job's own exit_status follows shell convention so the two ways a command can be unrunnable stay distinguishable: 127 when the command was not found, 126 when it was found but not executable.

Mesh identity

This repository is itself a Culture mesh agent, not only the product it builds: culture.yaml declares its suffix and backend, and AGENTS.colleague.md is the resident prompt file that backend loads (this agent runs backend: colleague). headspace doctor checks that pairing stays consistent. The .claude/skills/ kit is vendored cite-don't-import from guildmaster (and, for a few skills, directly from their true origin, devague) — see docs/skill-sources.md for per-skill provenance.

See CLAUDE.md for the full conventions (version-bump-every-PR, the cicd PR lane, deploy setup).

License

Apache 2.0 — see LICENSE.

Metadata

Release files for headspace-cli 0.10.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 headspace-cli 0.10.0
File Size Uploaded Provenance
headspace_cli-0.10.0.tar.gz 711.1 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for headspace-cli 0.10.0
File Interpreter ABI Platform
headspace_cli-0.10.0-py3-none-any.whl Python 3 none any Details

Total release size: 921.3 kB

Release files / headspace_cli-0.10.0.tar.gz

Download URL headspace_cli-0.10.0.tar.gz
Size 711.1 kB
Tags Source
SHA-256 checksum
How to use checksums
fc10677fe021351fd4d18e352b3475647da025c7e0139c51650888cc4e9e575a
BLAKE2b-256 checksum
How to use checksums
d33bf3115b7a0e05a857257a61c1b36b627f34128051f97904d28a77fd14063c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.0 {"installer":{"name":"uv","version":"0.12.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / headspace_cli-0.10.0-py3-none-any.whl

Download URL headspace_cli-0.10.0-py3-none-any.whl
Size 210.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
02dd70b6fd1ca85f5b5735e37f1255e37074b72c34d6df6deefde36e128225dd
BLAKE2b-256 checksum
How to use checksums
d5b30f80a5ead3fe46c8663eb753b266ddaa98cc0ddb8713f9a40e0a46ca3115
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.0 {"installer":{"name":"uv","version":"0.12.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Security for headspace-cli 0.10.0

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.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

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