Read-side access to Claude Code's on-disk background-job state.
Claude Code ships a supervisor daemon (claude daemon run) that
orchestrates background agent tasks launched via the claude agents
TUI. Per-task state lives under ~/.claude/jobs/<short_id>/:
state.json-- current snapshot: state, intent (original prompt), session id, link to the session JSONL, timestamps, auto-generated name, etc.timeline.jsonl-- append-only event log: at each state transition the daemon writes a line carrying timestamp, new state, a one-line detail, and (often) the full text body.
The session content itself is a normal
~/.claude/projects/<slug>/<session_id>.jsonl -- the same format
ClaudeWrapper.History parses. Each summary's session_path points
at it for direct cross-linking.
This module is read-only on purpose. The dispatch protocol (how the TUI launches new tasks) is undocumented and version-sensitive; mirroring it would defeat the drift defenses the wrapper relies on.
Liberal parsing
Like ClaudeWrapper.History, parsing is forgiving. list/1 skips
entries that are not directories (e.g. the daemon's pins.json),
that carry no state.json (a spare worker dir), or whose
state.json fails to parse -- malformed jobs are dropped rather
than failing the whole listing. The timeline parser skips blank and
malformed lines.
Example
{:ok, root} = ClaudeWrapper.Jobs.home()
{:ok, summaries} = ClaudeWrapper.Jobs.list(root)
for s <- summaries do
IO.puts("#{s.short_id} [#{s.state}] #{s.intent}")
end
{:ok, job} = ClaudeWrapper.Jobs.get(root, "90c961c7")
for event <- job.timeline do
IO.puts("#{event.at} #{event.state}")
end
Summary
Functions
Use a specific path as the jobs root. Useful for tests (point at a temp dir) and non-default installs.
Read one job by short id (its ~/.claude/jobs/<short_id>/ directory
name) into a full ClaudeWrapper.Jobs.Job, including the parsed
timeline.jsonl and the raw state.json map.
Resolve the default jobs root, ~/.claude/jobs.
List every job directory at the root, sorted by short_id.
The configured root directory.
Types
@type t() :: %ClaudeWrapper.Jobs{root: String.t()}
Functions
Use a specific path as the jobs root. Useful for tests (point at a temp dir) and non-default installs.
@spec get(t(), String.t()) :: {:ok, ClaudeWrapper.Jobs.Job.t()} | {:error, ClaudeWrapper.Error.t()}
Read one job by short id (its ~/.claude/jobs/<short_id>/ directory
name) into a full ClaudeWrapper.Jobs.Job, including the parsed
timeline.jsonl and the raw state.json map.
Returns {:error, %ClaudeWrapper.Error{kind: :not_found}} when no such
directory exists or its state.json is missing.
@spec home() :: {:ok, t()} | {:error, ClaudeWrapper.Error.t()}
Resolve the default jobs root, ~/.claude/jobs.
Returns {:error, %ClaudeWrapper.Error{kind: :no_home}} when the user
home cannot be determined.
@spec list(t()) :: {:ok, [ClaudeWrapper.Jobs.Summary.t()]} | {:error, ClaudeWrapper.Error.t()}
List every job directory at the root, sorted by short_id.
Returns {:ok, []} when the root directory does not exist (no
background agents have been launched on this machine yet). Skips
entries that are not directories, that carry no state.json, or
whose state.json fails to parse.
The configured root directory.