ps, stop, attach
Container lifecycle for heretic agents — label-based discovery, session filtering, JSON output, sidecar and network teardown, name matching rules, and detach keystrokes
All three commands discover containers by label, never by name pattern, so they only ever act on containers heretic created.
container name: heretic-<agent>-<session>-<hash8>
hash8: first 8 hex characters of sha256(<absolute project dir>)
| Label | Value |
|---|---|
heretic.managed | true on every container and network heretic creates |
heretic.agent | profile name |
heretic.project | absolute project directory |
heretic.session | session name (default unless -s was used) |
heretic.role | build-sidecar on builders, build-network on the per-run network |
heretic.runtime | node / python / java / go / rust on builders |
heretic.network | per-run sidecar network name, on both agent and builders |
heretic-cli ps
heretic-cli ps [--json] [-s|--session <name>]
Lists containers in all states, then removes the build sidecars from the output — builders are infrastructure, not agents.
$ heretic-cli ps
NAME AGENT SESSION STATUS UPTIME PORTS
heretic-claude-default-1a2b3c4d claude default running 4m ago 3000→3000
heretic-claude-feat-x-1a2b3c4d claude feat-x exited 2h ago -
| Column | Source |
|---|---|
| NAME | container name, or a 12-character ID when unnamed |
| AGENT | heretic.agent label, else unknown |
| SESSION | heretic.session label, else - |
| STATUS | Docker state: running, exited, created, paused, dead |
| UPTIME | age since creation, not time since start |
| PORTS | published mappings, else - |
--json prints the same fields as an array of objects (name, agent, session, status, uptime, ports); uptime and ports are pre-formatted strings.
Nothing running → No running heretic agents. (or []). Docker unreachable → Docker is not running or not available and exit 1.
UPTIME is age, not uptime
A container created two days ago and restarted a minute ago still shows 2d ago.
heretic-cli stop
heretic-cli stop [name] [--all] [-f|--force] [--keep] [-s|--session <name>]
| Form | Target |
|---|---|
stop | the agent container whose heretic.project equals the current directory (plus session when -s is given), then its sibling sidecars |
stop <name> | matched against agent containers only, then its sibling sidecars |
stop --all | every heretic-managed container, agents and sidecars |
Name matching, first match wins: the container ID starts with the string, or a container name equals it, or a container name contains it as a substring. So stop claude matches heretic-claude-default-1a2b3c4d; an ambiguous substring picks an arbitrary match.
Sibling sidecars are containers with heretic.role=build-sidecar, the same heretic.project, and the same heretic.session when the agent has one.
Unless --force, you get a confirmation whose default is yes:
Stop 3 container(s): heretic-claude-default-1a2b3c4d, heretic-builder-node-default-1a2b3c4d, … ? (use --force to skip) (Y/n)
Per container: an already exited/dead container reports Container X is already stopped; otherwise it is stopped with a 10 s timeout (Stopped X) and, unless --keep, removed (Removed X). One failure never aborts the rest — failures are collected and reported at the end (Errors occurred while stopping containers:) with exit 1.
Network cleanup: unless --keep, every distinct heretic.network among the stopped containers is removed. Named cache volumes are intentionally kept so builder caches stay warm.
heretic-cli stop # this directory's agent + its sidecars
heretic-cli stop -f # …without the prompt
heretic-cli stop claude --keep # stop but keep containers and network
heretic-cli stop --all -f # everything heretic-managed
heretic-cli stop -s feat-x -f # only session feat-x
Informational messages that are not errors (exit 0): No heretic containers found, No heretic container found matching '<name>', No heretic container found for the current directory.
stop with no argument matches the directory exactly
It compares heretic.project against the current working directory as a string. Running it from a subdirectory, or through a symlinked path, finds nothing — pass the container or agent name instead.
heretic-cli attach
heretic-cli attach <name> [-s|--session <name>]
Requires a running container, otherwise Container '<name>' is not running (state: <state>) and exit 1. Matching works like stop, but over all heretic containers — sidecars included, where attaching just shows the exec-server's output.
$ heretic-cli attach claude
Attached to heretic-claude-default-1a2b3c4d. Press Ctrl+P, Ctrl+Q to detach.
It shells out to docker attach, so the docker CLI must be on PATH, and heretic exits with docker attach's exit code. Ctrl+P, Ctrl+Q detaches without killing the session; Ctrl+C forwards SIGINT to PID 1 and usually ends it.
Attaching to a container started without interactive: true gives you output but no stdin. For a second, independent shell use docker exec -it <name> bash — remembering that a fresh exec does not inherit the entrypoint's HOME export under --root.
Cleaning up completely
heretic-cli ps # agents only
docker ps -a --filter label=heretic.managed=true # agents and sidecars
heretic-cli stop --all -f # stop + remove, drop per-run networks
docker network ls --filter label=heretic.role=build-network
docker volume ls # cache volumes are never auto-removed
Session state under .heretic/temp/<session>/ — including the agent's ~/.claude history — is never pruned. Delete those directories by hand to reset an agent.
There is no heretic-cli logs, exec, restart or rm: use docker logs heretic-<agent>-<session>-<hash8> and friends.