Skip to content

Instantly share code, notes, and snippets.

@possibilities
Created April 8, 2026 00:09
Show Gist options
  • Select an option

  • Save possibilities/501435beef05b6b977fc0d5acdc7bebb to your computer and use it in GitHub Desktop.

Select an option

Save possibilities/501435beef05b6b977fc0d5acdc7bebb to your computer and use it in GitHub Desktop.
How jobctl works — architecture, tmux integration, and prise status

How jobctl Works

jobctl is a read-only query tool for discovering and inspecting running Claude Code sessions ("jobs"). It has two commands: list-jobs and show-job.

Data Source

jobctl reads from a single SQLite database: ~/.local/state/claude/hooks-tracker.db. This database is populated by Claude Code's hook system (via hookctl), not by jobctl itself. jobctl never writes to it.

Key tables:

  • jobs — job_id, name, created_at, tmux coordinates, state
  • job_sessions — maps jobs to Claude Code session IDs and PIDs
  • events — hook events with permission_mode, tool_name, cwd
  • session_env — environment variables per session (including CLAUDE_PROJECT_DIR)

Job Discovery (list-jobs)

The core query in api.py:25-34 pulls all non-ended jobs, joining to job_sessions for the latest PID and session_id. It then validates each PID is alive via os.kill(pid, 0) — catching unclean exits where the SessionEnd hook never fired.

Scoping

By default, jobctl scopes results to:

  1. Current tmux session — only shows jobs whose tmux_session matches yours
  2. Current project — only shows jobs whose CLAUDE_PROJECT_DIR matches os.getcwd()

Flags override this: --all shows headless jobs too, --all-projects removes the project filter, --history includes ended/crashed jobs.

Job State Machine

jobctl derives display state from the DB state + PID liveness:

DB state PID alive? Display state
running yes working
running no crashed
stopped yes stopped
stopped no ended
ended ended

Mode Tracking

jobctl reads the events table to build a mode history. Events with permission_mode = "plan" map to "plan"; everything else maps to "act". The output shows both initial_mode and current_mode.

show-job

Takes an identifier (job name, job_id, session_id, or substring). If omitted and you're in tmux, it auto-detects the job in your current window. Shows full details including mode history, tmux target, session timeline, and waiting state.


Tmux Integration

Tmux integration is deep and bidirectional, but most of it lives in hookctl, not jobctl itself.

Reading Side (jobctl)

helpers.pyget_tmux_context() checks $TMUX, then runs tmux display-message -p "#{session_name}\n#{window_index}" to get the current session/window. This is used for scoping list-jobs and auto-detecting jobs in show-job.

Each job stores tmux_session, tmux_window, and tmux_pane in the database. list-jobs formats these as a tmux target: session:window.pane.

Writing Side (hookctl)

hookctl sync-tmux (run_sync_tmux.py) keeps the database in sync with tmux reality:

  1. Lists all panes via tmux list-panes -s to get pane_pid → (session, window, pane) mappings
  2. Builds a full process tree from ps -axo "pid=,ppid="
  3. Walks the tree to map every descendant PID back to its tmux pane
  4. Updates the jobs table if any coordinates changed
  5. Handles pane migrations — if a job's PID disappeared from its session, scans all sessions (covers break-pane / move-pane scenarios)

Triggered by tmux hooks and after place-pane moves.

hookctl place-pane (run_place_pane.py) is called by the on_job_named Claude Code hook. It:

  • Places the Claude pane in the correct tmux session/window
  • Detects conflicts (two Claude instances in the same window) — newer instance moves, older stays
  • Fires hookctl sync-tmux afterward to update coordinates

Prise Integration

jobctl has no prise integration. It operates exclusively through tmux.

Prise (the terminal multiplexer fork) is managed by a separate tool — prisectl (apps/prisectl/), which speaks msgpack-RPC to the prise socket at /tmp/prise-{uid}.sock. prisectl has its own commands (run-claude, start-pty, etc.) but doesn't feed into the hooks-tracker database that jobctl reads.

The two worlds are currently separate: tmux sessions are tracked by jobctl via hookctl's sync mechanism, while prise PTYs are managed by prisectl independently.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment