Skip to content

crush

Crush (catalog agent crush) is Charm's terminal coding agent. It began as an OpenCode fork (opencode.md) but no longer shares any storage code with it: Crush is Go, with sqlc and goose migrations, and keeps one database per project where OpenCode keeps one per user (OpenCode's data directory); it has no share service (OpenCode's share records), no permission JSON on the session (OpenCode's permission records), no agent or mode recorded per message (OpenCode's messages), and file history in a files table instead of git snapshots. Hyper is Charm's hosted model gateway, and its Prism router picks a model per request. The analyzer parses the per-project crush.db, every crush.json scope and the crushrc shell configs, from source and fixtures only.

Source and evidence

charmbracelet/crush, open source, at ca6ae26ce016b980407ce32f012a467ec10c1e2f, Functional Source License 1.1 with MIT future license (LICENSE.md). All claims from source. Paths cited are relative to the clone. Between ca6ae26 and 7f799fb the lines of internal/config/load.go moved about 50 lines up; every path it names is unchanged.

Observed

No real install.

Source reviews

  • 2026-10-21: charmbracelet/crush at 7f799fb, with charmbracelet/fantasy at ec25857 (v0.45.1, the version go.mod pins): the fantasy copy settled that the hook metadata reaches the stored result, the dead ~/.config/crush/.crushrc went from Confidence to Per-user storage, and the parser plan's permission, relay, deletion, file and bad-line subsections now describe rows the parser writes; a source-host subsection was added.
  • 2026-10-19: 7f799fb, the default branch that day, against ca6ae26. Paths, migrations, columns and part keys were unchanged; the review added the permission outcomes, synthetic results, model columns, channels, sub-agent ids and MCP overrides in Record schema, the data-directory files of Other stores, the scratch directories and the parser plan's requirement subsections.
  • 2026-10-17: 12f96b4, a check for sync state. It found none and added the telemetry, Hyper and client-server notes of Sync, relay and origin rows and the delete behaviour of What survives a delete.

Collection

Per-user storage

Home comes from os.UserHomeDir() and the config root is XDG_CONFIG_HOME or ~/.config on every OS, Windows included (internal/home/home.go:12,26-31).

Config: ~/.config/crush/crush.json, or $CRUSH_GLOBAL_CONFIG/crush.json (internal/config/load.go:1212-1216), with an optional sibling shell config crushrc (load.go:1219-1231). A .crushrc in the config directory is never read: the global list holds the system config, the global crush.json, its crushrc sibling and the data crush.json, and the names .crushrc, crushrc, .crush.json and crush.json are searched only from the working directory up to the project boundary (load.go:946-975). Global context files default to ~/.config/crush/CRUSH.md and ~/.config/AGENTS.md (load.go:578-582).

Commands are read from ~/.config/crush/commands, ~/.crush/commands and <data dir>/commands (internal/commands/commands.go:124-133). Skills from $CRUSH_SKILLS_DIR, ~/.config/crush/skills, ~/.config/agents/skills (load.go:1366-1372); at 7f799fb also ~/.agents/skills and Claude Code's ~/.claude/skills (internal/config/load.go:1312-1322; shared.md). System config /etc/crush/crush.json on Unix, none on Windows (internal/config/config_unix.go:7, config_windows.go:8).

Data: GlobalConfigData() is $CRUSH_GLOBAL_DATA/crush.json, $XDG_DATA_HOME/crush/crush.json, %LOCALAPPDATA%\crush\crush.json on Windows, else ~/.local/share/crush/crush.json (load.go:1259-1281). The directory also holds projects.json (internal/projects/projects.go:14,31-32) and a locks/ directory (internal/config/store.go:1102). It is "writable machine state and must never be executed as Bash" (load.go:947-949). There is no session database here; sessions live in each project.

Cache: $CRUSH_CACHE_DIR, $XDG_CACHE_HOME/crush, %LOCALAPPDATA%\crush\cache on Windows, else ~/.cache/crush (load.go:1235-1250), containing only server-<host>/crush.log for client-server mode (internal/cmd/server.go:50, internal/cmd/root.go:508,627).

Environment variables: CRUSH_GLOBAL_CONFIG, CRUSH_GLOBAL_DATA, CRUSH_CACHE_DIR, CRUSH_SKILLS_DIR, CRUSH_HYPER_API_KEY, XDG_*.

At 7f799fb the data crush.json also records the chosen models and recent models, and any crush.json and any crushrc can carry standing permission state; both are described in crush.json and crushrc.

crush server loads its configuration with the data directory itself (~/.local/share/crush, Windows %LOCALAPPDATA%\crush) as its working directory (internal/cmd/server.go:40, internal/config/load.go:1228-1235); whether that ever creates a .crush database there was not determined from source. The catalog collects the whole directory either way.

Scratch directories

Crush makes two working directories of its own, both inside the project data directory: .crush/crush-fetch-*, the working directory of the web-fetch sub-agent, created with os.MkdirTemp in the data directory and removed when the fetch ends (internal/agent/agentic_fetch_tool.go:102-106, :142), and .crush/shell-output, oversized command output kept seven days and capped at 256 MB (internal/shell/truncate.go:29-43). Both are collected with .crush. With no data directory the spill falls back to the system temp directory (:145-151), which no catalog line reaches; every caller found passes the data directory. Sessions record no working directory, so none of these can be mistaken for a project (Scratch directory rows).

Credentials

  • ~/.local/share/crush/crush.json (Windows %LOCALAPPDATA%\crush\crush.json): the TUI writes provider keys and OAuth tokens here. SetProviderAPIKey (store.go:569) resolves the global scope to globalDataPath (store.go:330-341); ProviderConfig has api_key (config.go:102) and oauth (config.go:106); MCP entries persist oauth_token (config.go:273-275, orphan cleanup config.go:326-328); the Hyper provider token is refreshed in this file (load.go:109).
  • ~/.config/crush/crush.json can hold the same api_key fields literally or as $ENV references.
  • ~/.config/crush/crushrc and project crushrc/.crushrc are shell scripts that typically export keys; each is flagged.
  • A project's crush.json and .crush.json are config scopes with the same api_key fields (Project-local files); both are flagged.
  • Project .crush/crush.json is the workspace scope and SetProviderAPIKey can target it (store.go:333-337).
  • Crush also reads ~/.config/github-copilot/apps.json (%LOCALAPPDATA%\github-copilot\apps.json) for Copilot OAuth (internal/oauth/copilot/disk.go:32-34) and probes ~/.aws/credentials and ~/.aws/login (load.go:1118-1121). No keychain use.
  • No new credential file at 7f799fb. The project lock file .crush/crush.lock is not one (Data directory files).

Exclusions

Nothing large under home. In projects, .crush/crush-fetch-* are temporary fetch directories (internal/agent/agentic_fetch_tool.go:102) and .crush/shell-output holds spilled tool output (internal/shell/truncate.go:29), bounded at 256 MB and seven days since 7f799fb (Scratch directories); both are evidence of what the agent fetched and ran, so neither is an exclusion. ~/.cache/crush holds only the client-server log, and stats/index.html is one small page. Nothing new to exclude at 7f799fb.

Project-local files

Data dir is the closest .crush between cwd and the git root, else <cwd>/.crush (config.go:25, load.go:590-593). Contents: crush.db with -wal/-shm (internal/db/connect.go:20,93), crush.json workspace overrides (load.go:58, store.go:97), logs/crush.log (root.go:311), commands/, shell-output/, crush-fetch-*, and a .gitignore containing * (root.go:295-297), so git never sees it.

Config files searched from cwd up to the git root: .crushrc, crushrc, .crush.json, crush.json (load.go:962-970).

Context files: .github/copilot-instructions.md, .cursorrules, .cursor/rules/, CLAUDE.md, CLAUDE.local.md, GEMINI.md, gemini.md, crush.md, crush.local.md, Crush.md, Crush.local.md, CRUSH.md, CRUSH.local.md, AGENTS.md, agents.md, Agents.md (config.go:29-45).

At 7f799fb the data directory also holds crush.lock, the lock that records the last start (internal/db/datadirlock.go:23), an empty init flag marking the project initialised (internal/config/init.go:15) and, after crush stats, stats/index.html, a usage summary (internal/cmd/stats.go:232); their contents are in Data directory files. The log is now at internal/cmd/root.go:324.

Ignore file .crushignore (internal/fsext/ls.go:141,168).

Where the project path is recorded

projects.json next to the data crush.json (~/.local/share/crush/projects.json, Windows %LOCALAPPDATA%\crush\projects.json): projects[].path, projects[].data_dir, projects[].last_accessed (projects.go:18-25), registered on every start (root.go:302). At 7f799fb a registration moves the project to the front of the list instead of sorting by last_accessed, so the order is most recently opened first (internal/projects/projects.go:84-96); the keys are unchanged (:17-21). crush stats itself crawls for .crush/crush.db (internal/cmd/stats.go:247-265), confirming the layout.

Parsing

Transcript stores

Per project: <project>/.crush/crush.db (internal/config/config.go:25 default .crush; internal/db/connect.go:93 crush.db). The data directory is found by walking up from the cwd (internal/config/load.go:590-593) and can be overridden by data_directory (config.go:470). The global registry projects.json maps each project to its data directory (Where the project path is recorded). One messages row per message; parts are a JSON array in the parts column.

What survives a delete

crush session delete and the TUI's delete remove one session's messages, files and sessions rows in one transaction, and its read_files rows by foreign key, with secure_delete zeroing the freed pages (internal/session/session.go:144-168; 20260127000000_add_read_files_table.sql:7; internal/db/connect.go:25). At 12f96b4 the same delete is one query and two cascades (internal/db/sql/sessions.sql:79-81, 20250424200609_initial.sql:32, 20250424200609_initial.sql:56).

The deleted session's sub-agent sessions stay in crush.db with their messages, since parent_session_id has no foreign key and the delete names one id, so a parent_session_id that names no session is the trace of a deleted parent. Crush keeps no per-prompt history file and no second copy of the transcript; logs/crush.log and the stats/index.html snapshot, which still counts the deleted session, outlive the delete. The session deleted telemetry event goes to the vendor without a session id (internal/session/session.go:173, internal/event/all.go:28-30), and nothing on disk records it. The analyzer reads the database, the sub-agent sessions included, and writes a session deleted: row for each parent a sub-agent session names that is gone (Deletion rows); it reads neither the log nor the stats page (see Secondary sources).

Record schema

internal/db/migrations/20250424200609_initial.sql.

sessions

sessions (:4-14 plus ALTERs): id (uuid; task sub-sessions use the tool call id, internal/session/session.go:103,117), parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at (epoch seconds, strftime('%s','now'), internal/db/sql/sessions.sql:22-23), summary_message_id, todos JSON, channel.

messages and parts

messages (:47-57 plus ALTERs; internal/db/models.go:29-44): id uuid (internal/message/message.go:206), session_id, role in assistant|user|system|tool (internal/message/content.go:20-27), parts JSON, model, provider (20250627000000_add_provider_to_messages.sql), created_at, updated_at, finished_at (seconds, copied from the finish part's time, message.go:435-439), is_summary_message, prism_model_id, prism_model_name, prism_*_savings.

parts is [{"type":...,"data":{...}}] (message.go:613-629). Types and data keys (content.go): text: text, hidden? (:70-74); reasoning: thinking, signature, thought_signature, tool_id, responses_data, started_at, finished_at (seconds; :55-63); tool_call: id, name, input (JSON string), provider_executed, finished (:109-115); tool_result: tool_call_id, name, content, data (base64 media), mime_type, metadata, is_error (:119-127); finish: reason in end_turn|max_tokens|tool_use|canceled|error|content_filter|unknown, time (seconds), message?, details? (:33-49,131-136); shell_command: command, output, exit_code (:142-146); image_url: url, detail; binary: untagged Go fields Path, MIMEType, Data (:93-97).

Other tables

files (session file snapshots: path, content, version), read_files, mcp_enabled_servers, mcp_disabled_servers. No project path, cwd or git branch column anywhere.

The columns and part keys above are unchanged at 7f799fb, which the subsections that follow cite.

Permission outcomes in tool results

The permission service keeps every decision in memory: the per-session grant ("allow for session", GrantPersistent), the allow list and the skip flag behind --yolo (internal/permission/permission.go:165-197), and none of it is written to a table. A call the user denies returns User denied permission as an error result that ends the turn (internal/agent/tools/tools.go:78-84), stored as a tool_result part with is_error: true (internal/agent/agent.go:2155-2172).

A PreToolUse hook (a user command configured under hooks in crush.json) that denies or halts a call returns Tool call blocked by hook. Reason: <reason> or Turn halted by hook. Reason: <reason>, and every hooked result carries {"hook": {"hook_count", "decision", "halt", "reason", "input_rewrite", "hooks": [{"name", "matcher", "decision", ...}]}} in its metadata, decision being allow, deny or none (internal/agent/hooked_tool.go:62-73, :98-140, internal/hooks/hooks.go:26-40, :56-65). The hook object reaches the stored result: fantasy copies the tool response's Metadata to the result's ClientMetadata on both the error and the success path (agent.go:851, :859), and Crush stores ClientMetadata as the part's metadata, a JSON string (internal/agent/agent.go:2155-2160).

Synthetic results

internal/agent/settle.go, new since ca6ae26, settles tool calls that never got an answer (a cancel, a crash, a truncated or refused response) with an error tool_result in a new tool message whose text is one of a fixed set (Error: user cancelled assistant tool calling, tool call was interrupted and did not produce a result... and four others), and on reopening a session after a dead process gives the unfinished assistant message a finish of reason error, message Interrupted, backdated to the message's updated_at (internal/agent/settle.go:14-24, :140-170). The synthetic result's message is created at repair time, so it can be stamped long after the call.

Model columns

messages.model and provider are the configured model of the turn (internal/agent/agent.go:557-562); prism_model_id and prism_model_name are the model that actually served it when the turn went through Hyper's Prism router, from its response headers (:1085, :1914-1929; 20260902000000_add_prism_model_to_messages.sql:3-8). So model is the request and prism_model_id the resolved id. The Prism layout is not observed on any real install, source-derived. No per-session model and no switch record exist; the plan and code modes and yolo mode the TUI toggles are in memory only (internal/ui/model/ui.go:4762-4790), and the reasoning effort and thinking flag reach only telemetry.

Channels (relay)

An MCP server that declares the claude/channel capability pushes events into the active session as a user message whose text is <channel source="<server>" <meta attributes>>content</channel> (internal/agent/tools/mcp/channel.go:20-36, :124-150; internal/agent/agent.go:1550-1551), sessions.channel holds the server of the latest channel-started turn and is cleared by the next local turn (internal/agent/coordinator.go:436-465), and at the end of such a turn Crush sends the reply through one of the server's tools, chosen by mcp.<name>.channel_reply in crush.json or discovered from the tool list (the latter only in yolo mode); the sender and group meta attributes name the recipient (internal/agent/channelreply.go:23-40, :245-307). The rendered element is the turn's prompt as it stands, so the stored user text starts <channel source= (internal/backend/channels.go:75-96). The source names a Signal MCP server as the model case, so a channel session can be carried through a messaging platform. The automatic reply is not a stored tool call: only the log records it (Data directory files).

Sub-agent sessions

A sub-agent's session id is <message id>$$<tool call id> with parent_session_id set (internal/session/session.go:116-128, :427-430; internal/agent/coordinator.go:1667-1668). parent_session_id has no foreign key (20250424200609_initial.sql:4-14).

Repository-scoped MCP overrides

mcp_enabled_servers and mcp_disabled_servers hold one name each, an MCP server the user turned on or off for this project, overriding crush.json (20260928000000_add_mcp_disabled_servers.sql:3-5; internal/session/session.go:403-415).

Joins and timestamps

messages.session_id → sessions.id; sessions.parent_session_id for sub-agents. tool_call parts live in the assistant message; the result is a separate role='tool' message with a tool_result part (internal/agent/agent.go:1032-1038), joined on tool_result.data.tool_call_id = tool_call.data.id. Project path is the parent directory of the .crush holding the database, or projects.json projects[].path / data_dir.

Other stores

SQLite journal and deletes

journal_mode=WAL (connect.go:20) with Go defaults (auto-checkpoint at 1000 pages), so a copy without crush.db-wal loses recent messages. secure_delete=ON (connect.go:25) zeroes deleted rows. Writes are debounced about 33 ms (message.go:21) and serialised on one connection (connect.go:142). No encryption or compression.

Data directory files

Other files in the data directory, at 7f799fb; none repeats the transcript, so none is a secondary source:

crush.json and crushrc

Standing permission state, at 7f799fb: permissions.allowed_tools (tools or tool:action pairs that skip the prompt), options.disabled_tools (tools hidden from the agent), mcp.<name>.disabled_tools and enabled_tools, and hooks (the PreToolUse commands) in any crush.json scope (internal/config/config.go:224-225, :431-433, :471, :865-869; internal/app/app.go:103-106), plus the same two lists set by permissions allow <tool>... and permissions deny <tool>... lines in a crushrc shell config (internal/shellconfig/permissions.go:10-78), and the per-project mcp_enabled_servers and mcp_disabled_servers tables. --yolo (-y) is a command-line flag held in memory (internal/cmd/root.go:61, :301), never written. The crush.json files and crushrc are collected and parsed (Permission rows).

The global data crush.json keeps the user's current models.large and models.small and up to five recent_models per type (internal/config/config.go:850-854, internal/config/store.go:521-547), a preference, not a per-session record.

Format versions

Goose migrations only (12 files, the same set at ca6ae26 and 7f799fb); no JSON era, no version field; goose_db_version table. The one record shape added between those commits is the synthetic result.

Secrets

crush.json providers.<name>.api_key (config.go:102) at ~/.config/crush/, ~/.local/share/crush/ and <project>/.crush/. reasoning.signature and thought_signature are opaque provider blobs. Tool input and output can contain pasted tokens.

Parser plan

Globs: .local/share/crush/projects.json, AppData/Local/crush/projects.json, project-level .crush/crush.db{,-wal,-shm}, every crush.json scope (.config/crush/crush.json, .local/share/crush/crush.json, AppData/Local/crush/crush.json, .crush/crush.json, a project's crush.json and .crush.json), and .config/crush/crushrc and a project's crushrc and .crushrc.

Rows: one per part in parts.

Column Source
timestamp_utc messages.created_at (s); tool_call and tool_result use finished_at or finish.time
session_id sessions.id
project_path directory containing .crush, or projects.json path
git_branch empty
turn_type user/assistant text; reasoning → thinking; tool_call → tool_use; tool_result → tool_result; role system, finish, shell_command → system
model provider/prism_model_id when set, else provider/model
tool_name, tool_use_id data.name, data.id or data.tool_call_id
text text, input JSON, output

Permission rows

Parsed; source-derived; not observed on any real install.

Per call (primary). After a tool_result row, with its tool_name and tool_use_id, the parser writes permission decision: denied by user when the error content is User denied permission; permission decision: <allow|deny|halt> by hook <names> reason=<reason> from metadata.hook, where halt wins over the decision, <names> are the hooks whose own decision (or halt) matches, and  reason= is present only when the hook gave one; and permission decision: <deny|halt> by hook for an error result that starts Tool call blocked by hook. or Turn halted by hook. but carries no hook metadata (Permission outcomes in tool results). A hook decision of none without a halt writes nothing. A prompt the user approved, a session grant, the allow list and yolo mode leave nothing, so the absence of a denial is not an approval.

Standing (primary). One permission setting: <key>=<value> (from <file>, file mtime) row per entry at the file's modification time, with an empty session_id (crush.json and crushrc): from every crush.json scope the entries of permissions.allowed_tools, options.disabled_tools, mcp.<name>.disabled_tools, mcp.<name>.enabled_tools and each hook under hooks.<event> (the hook object as compact JSON); from a crushrc, each tool on a permissions allow line as permissions.allowed_tools=<tool> and on a permissions deny line as options.disabled_tools=<tool>, with (from <file> line <n>, file mtime); from crush.db, mcp_enabled_servers=<name> and mcp_disabled_servers=<name> at the database file's modification time. The crushrc reader takes only lines whose first word is permissions, as written, so a line inside a condition that never ran still yields its rows. A workspace .crush/crush.json and a project file carry the project as project_path; no other crush.json key is read, so provider keys and tokens never reach a row.

Model rows

Primary, parsed. The served model of a message is prism_model_id when set, else model. A system row model: <served> provider=<provider> precedes the first message of a session that names a model, and model: <old served> -> <new served> provider=<provider> every later message of the session whose served model, requested model or provider differs, with the message's created_at;  requested=<model> follows when the requested model differs from the served one (a Hyper Prism turn: model: claude-sonnet-4-5 provider=hyper requested=<model>). A message with no prism_model_id that repeats the session's last provider and requested model is not a change, so a Hyper step without Prism headers does not flip the row back to the request. tool messages store an empty model and are skipped.

No per-session model or switch record exists (Model columns), and the models.large, models.small and recent_models preferences of the data crush.json are not a per-session record (crush.json and crushrc).

Past-use rows and secondary sources

Crush keeps none that repeats the transcript: no prompt history file (the TUI's prompt history is a query over messages, internal/ui/model/history.go:18-30), no search index, no export. Evidence of other kinds: files (each version of every file the agent wrote, per session) and read_files (each file read, per session, with read_at), both deleted with their session, are parsed, as file version: <path> version=<n> bytes=<length> (from crush.db files) at the snapshot's created_at (the content stays in the database) and file read: <path> (from crush.db read_files, read_at) at read_at, source-derived; not observed on any real install. Neither repeats a transcript event, so neither is secondary. logs/crush.log and stats/index.html (Data directory files), which outlive the session, are not parsed. Compaction (is_summary_message) adds a summary message and keeps the earlier ones; the model reads from the summary onward (internal/db/sql/messages.sql:79), so nothing is lost locally.

Sync, relay and origin rows

Sync: Crush records no sync state, at 12f96b4 and again at 7f799fb. No command shares, exports, publishes or uploads a session: the subcommands are run, dirs, projects, update-providers, logs, logout, schema, login, stats and session (internal/cmd/root.go:67-78; unchanged at 7f799fb, internal/cmd/root.go:68-79), plus server and models registered in their own files, and session has only list, show, last, delete and rename (internal/cmd/session.go:34-97). No migration adds a remote id, share URL, synced flag or sync time to any table, at either commit (the twelve files under internal/db/migrations/; the newest sessions column, channel, is 20260921000000_add_channel_to_sessions.sql:2). Searched share, publish, upload, cloud, sync, teleport, backup, export, remote_id, share_url, synced and gist over the Go and SQL sources. What does leave the host, besides the model calls:

The parser plan proposes no sync row.

Relay (primary, parsed; source-derived; not observed on any real install). A session whose sessions.channel is set, or whose user text starts with <channel source=, was driven by an MCP channel (Channels (relay)). The parser sets records_relay and writes session relayed: channel <server> (from crush.db sessions.channel, as of updated_at) after the session start of a bound session, at its updated_at, and session relayed: channel <source> sender <sender> group <group> (from crush.db message <id>) after the first user message of a session for each distinct source, sender and group, with sender and group present only when the element carries them. Whether the channel is a messaging platform is only in the server's name and the <channel> attributes. A channel turn's automatic reply is in the log alone.

Origin: none; every session is created by the local process.

Scratch directory rows

The scratch directories appear only in the web-fetch sub-agent's tool inputs. Since sessions record no working directory and the parser takes the project from the database's location, no scratch path can become a project; no scratch directory: row is needed.

Deletion rows

Parsed; source-derived; not observed on any real install. A session whose parent_session_id names no session is the trace of a deleted parent (What survives a delete). The parser writes one row per such parent, session deleted: <parent> (parent_session_id of sub-agent session <id> names no session in crush.db; time is the sub-agent's updated_at), with session_id the deleted parent, so the parent appears as a session of its own; the delete time is not recorded, and the sub-agent's updated_at is only a time the parent still existed before. The log, the stats page and the telemetry event session deleted (no session id) outlive the delete and are not read.

Bad-line rows

A crushrc is read line by line for its permissions lines: such a line that does not split into shell words (an unclosed quote) is one row parser: unparseable line (<reason>): <line> with source_line its line number, and the file's other lines are still read; other lines are not records and are skipped. projects.json that does not parse or cannot be read is one whole-file parser: row, as is a crush.json that is not a JSON object (parser: <file> is not a JSON object); a messages.parts value that does not decode (not JSON, or JSON that is not an array) is one system row parser: unparseable record <rowid> (message <id> parts: <reason>): <parts>, carrying the stored value in full, with the message's session and project, an empty timestamp and source_line set to the rowid; the session's other messages are still yielded (issue 166).

Fixtures

Fixture rows:

INSERT INTO sessions(id,title,message_count,prompt_tokens,completion_tokens,cost,updated_at,created_at) VALUES('6f1c0001','Fix bug',3,10,5,0.01,1760000005,1760000001);
INSERT INTO messages(id,session_id,role,parts,model,provider,created_at,updated_at,finished_at,is_summary_message) VALUES
('m1','6f1c0001','user','[{"type":"text","data":{"text":"list files"}},{"type":"finish","data":{"reason":"stop","time":0}}]','claude-sonnet-4','anthropic',1760000001,1760000001,NULL,0),
('m2','6f1c0001','assistant','[{"type":"tool_call","data":{"id":"call_x1","name":"bash","input":"{\"command\":\"ls\"}","provider_executed":false,"finished":true}},{"type":"finish","data":{"reason":"tool_use","time":1760000003}}]','claude-sonnet-4','anthropic',1760000002,1760000003,1760000003,0),
('m3','6f1c0001','tool','[{"type":"tool_result","data":{"tool_call_id":"call_x1","name":"bash","content":"a.txt","data":"","mime_type":"","metadata":"","is_error":false}},{"type":"finish","data":{"reason":"stop","time":0}}]','','',1760000003,1760000003,NULL,0);

projects.json:

{"projects":[{"path":"/home/u/repo","data_dir":"/home/u/repo/.crush","last_accessed":"2026-10-01T12:00:00Z"}]}

Permission, relay, deletion and file records, invented values in the shapes above (a hook denial shown; the user denial is the content User denied permission with is_error: true and an empty metadata):

{"type":"tool_result","data":{"tool_call_id":"call_d2","name":"bash","content":"Tool call blocked by hook. Reason: no rm","metadata":"{\"hook\":{\"hook_count\":1,\"decision\":\"deny\",\"reason\":\"no rm\",\"hooks\":[{\"name\":\"guard\",\"matcher\":\"bash\",\"decision\":\"deny\"}]}}","is_error":true}}
<channel source="signal" group="g-77" sender="+15550100">deploy status?</channel>

A sub-agent session m9$$call_t1 with parent_session_id 6f1c0099 and no such session; a crush.json with permissions.allowed_tools, options.disabled_tools, mcp.docs.disabled_tools, mcp.docs.enabled_tools and one hooks.PreToolUse entry beside a provider api_key; a crushrc:

permissions allow view 'bash:ls'
permissions deny sourcegraph

Source host rows

The agent records no source-host integration. Searched github, gitlab, bitbucket, oauth, installation, repositor, pull request and clone over the Go sources at 7f799fb: the only GitHub account Crush holds is the Copilot OAuth grant it reads from github-copilot/apps.json to reach the Copilot models (Credentials), a model-provider credential, not repository access; the gh CLI is only probed on PATH to word the tool descriptions (internal/agent/tools/tools.go:86-93), and GitHub's MCP endpoint appears only in a list of servers that keep no MCP session (internal/config/config.go:628-631), so a GitHub MCP server is an ordinary user-configured MCP entry. No repository list and no repository-bound remote conversation is stored.

Confidence

  • High: all paths and file names, the table columns, the part JSON field names, the timestamp units, WAL, the tool result as a separate message and the absence of a git branch (source).
  • Medium: the Hyper Prism columns (prism_model_id, prism_model_name) are not observed on any real install, source-derived (Model columns).
  • Medium: the permission, relay, deletion, file and standing-setting rows rest on source alone; the hook, channel and Hyper surfaces are unexercised, and a real-install round is still owed.
  • Not determined: the binary part key casing in shipped builds (inferred from an untagged Go struct).