Architecture
claustre is a single-binary Rust application with seven modules, each with one responsibility. This page describes how the pieces fit together: the module layout, entity model, hook-based communication, and the session lifecycle.
Module Overview
Each module owns a single concern. The binary compiles to one executable with no runtime
dependencies beyond SQLite (bundled), gh CLI, and optionally npx
for skills.
| Module | Purpose |
|---|---|
main.rs | CLI entry point (clap). Dispatches to TUI or subcommands |
config/ | Config loading (config.toml), CLAUDE.md merge, path helpers |
store/ | SQLite database: schema, models, CRUD queries |
tui/ | ratatui terminal UI: app state, event loop, rendering |
session/ | Git worktree lifecycle + session setup |
pty/ | Native PTY embedding via portable-pty + vt100 |
skills/ | skills.sh CLI wrapper, ANSI parser |
Entity Model
Relationships
Project 1──* Task 1──* Subtask
Project 1──* Session
Task *──0..1 Session (assigned via session_id FK) Entities
- Project — a git repository registered in claustre. Has a
nameandrepo_path. - Task — a unit of work belonging to a project. Has a
title,description,status,mode(autonomous/supervised), and an optionalsession_idlinking it to the session executing it. Tracks token usage (input_tokens,output_tokens) and timing (started_at,completed_at). - Subtask — an optional breakdown of a task into steps. When a task has subtasks, they are all included in the prompt as an ordered list. Claude works through them sequentially in a single session.
- Session — a running Claude Code instance tied to a project. Maps
1:1 to a git worktree + embedded terminal tab. Tracks
claude_status(idle/working/done/error) and git diff stats (files_changed,lines_added,lines_removed).
Communication Architecture
claustre uses Claude Code's hook system and CLI subcommands instead of an MCP server.
Hooks fire after each Claude turn and call back into the claustre binary
to update state in SQLite. The TUI polls the database every second to pick up changes.
┌─────────┐ hooks ┌──────────────────┐ writes ┌──────────┐ reads ┌─────────┐
│ Claude │ ──fires──> │ claustre │ ────────> │ SQLite │ <──poll── │ TUI │
│ Session │ │ session-update │ │ (WAL) │ │ (1s) │
└─────────┘ └──────────────────┘ └──────────┘ └─────────┘ Hooks
Each worktree gets three hooks registered in .claude/settings.local.json.
The TaskCompleted and Stop hooks source a shared
_claustre-common.sh helper.
TaskCompleted hook
Progress sync
- Reads Claude's internal task progress from
~/.claude/tasks/<session_id>/and writesprogress.jsonto~/.claustre/tmp/<session_id>/ - Calls
claustre session-update --session-id <ID>(no token extraction — deferred to Stop hook)
Stop hook
Final validation + usage
- Final sweep of task progress and writes
progress.json - Extracts cumulative token usage from Claude's JSONL conversation log
- Checks for an open PR on the current branch via
gh pr view - Calls
claustre session-update --session-id <ID> [--pr-url <URL>] [--input-tokens N --output-tokens N --cost F]
UserPromptSubmit hook
Resume signal
- Reads session ID from
.claustre_session_id - Calls
claustre session-update --session-id <ID> --resumed - If the session has an
in_reviewtask, transitions it back toworking
Task Status Lifecycle
pending ──[launch]──> working ──[PR detected]──> in_review ──[PR merged]──> done
↑ │
└───[user resumes]────────────┘ | Transition | Trigger | Where |
|---|---|---|
pending → working | User presses l (launch), or feed-next picks up next task | session::create_session(), main::run_feed_next() |
working → in_review | Stop hook detects a PR via gh pr view and calls claustre session-update --pr-url | main.rs SessionUpdate handler |
in_review → working | UserPromptSubmit hook detects user activity and calls session-update --resumed | main.rs SessionUpdate handler |
in_review → done | PR merge poller detects merge (auto), or user presses r (manual) | tui/app.rs poll + key handler |
Session Creation Flow
When a user presses l on a pending task, claustre executes the following
sequence to stand up an isolated environment and launch Claude:
create_worktree()— runsgit worktree addfrom the project repo to create an isolated working copywrite_merged_config()— merges global + projectCLAUDE.md, copies hooks into the worktreestore.create_session()— inserts a session row in the database- Write session marker — writes
.claustre_session_idand hook scripts into the worktree pre_trust_worktree()— seeds~/.claude.jsonto skip the trust dialog- Return
SessionSetup— contains session, claude command, worktree path, and tab label - TUI spawns terminals — creates
SessionTerminals(shell + Claude PTYs) and adds a session tab
Key Patterns
State refresh via polling
The TUI runs a 1-second tick. On each tick, refresh_data() re-queries the
database to pick up any changes from the Stop hook, session-update, or
feed-next. This is simpler than cross-thread channels and provides
acceptable dashboard latency.
Pre-fetched sidebar summaries
build_project_summaries() queries session and task data for all projects
up front and stores results in a HashMap<String, ProjectSummary>.
This avoids N+1 queries during rendering.
Config inheritance
Worktree config is assembled at session creation time in
session::write_merged_config(). The merge order is:
- Global
CLAUDE.md(~/.claustre/CLAUDE.md) - Project-level
CLAUDE.md(~/.claustre/projects/<name>/CLAUDE.md) - Repository
CLAUDE.md(checked into the repo)
Hooks follow the same pattern: global hooks are copied first, then project-specific hooks override by filename.