cokecoder handbook wiki

Hooks

Technical chapter.hooks raw

A hook is a small program the ccc-statusd daemon runs when something happens to one of your agent sessions: a Claude Code event fires, the daemon joins a new session to your sessions root, a token or idle sensor crosses a line, or a question to you has gone unanswered for a while. The hook answers with a reply — text injected into the agent's context, an agent to dispatch, or a verdict that blocks a tool call or a stop.

The programs live in markdown, not in a config file. There is no $UCC_HOME/hooks.toml. The file is HOOKS.md, and what is on disk is what runs — nothing is compiled, cached or armed separately.

flowchart LR
  E[event or sensor] --> M[HOOKS.md mount] --> H[hook block] --> R[reply]

Two halves: transport and program

Half Where Written by What it decides
Transport each profile's Claude Code settings.json ccc-statusd hook install which Claude Code events reach the daemon at all
Program HOOKS.md on the sessions root, a session dir, or an agent home you, with a put what happens when one does

hook install registers the 14 events the daemon has a consumer for. The other 17 are opt-in per profile with hook install --with <Event> — each one mounted costs a subprocess per occurrence, so they are off until a hook needs them. A HOOKS.md hook naming an event the transport does not carry resolves but never fires; ccc-statusd hooks check marks it ⚠ transport-not-installed.

ccc-statusd doctor --ping starts every hook command registered in settings.json and names the ones that cannot exec. Claude Code swallows a hook command that fails to start, so without this check every file-reading probe reports a healthy plane over a dead one.

The three mounts

One filename, three places, merged by hook name — the deepest wins:

  1. <sessions-root>/HOOKS.md — the fleet page, every session on this root.
  2. <session-dir>/HOOKS.md — one session.
  3. <sessions-root>/agents/<short-id>/HOOKS.md — one agent.

A deeper mount overrides a hook either by declaring a block with the same name or by a frontmatter key hook.<name>.<suffix>, with the suffix one of six: on, match, model, role, timeout, budget. hook.<name>.on: "" turns a hook off for that scope. A key naming a hook no wider mount declares is reported as ⚠ override-orphan.

The fleet page is guarded: a put on it needs force: "<reason>" in its frontmatter. Session and agent pages are yours to write freely.

Triggers

A block's declare(on = "...") names its trigger:

Trigger Fires
a Claude Code event, verbatim PascalCase — UserPromptSubmit, PreToolUse, Stop, SessionStart, … (31 in all) when Claude Code emits it; each event has its own input keys, allowed reply keys and timeout
Join the daemon's own event: the first user prompt that creates the session directory and agent home (SessionStart is too early for card creation). Missing cards are retried at most once per 10 minutes, so a Join hook must be safe to repeat. A fork gets its own Join; a subagent shares its parent's session and gets none
tokens/N every N tokens of context
tokens>N once, on the upward crossing of N; a rewind or /compact re-arms it
idle>D*B after D of idle time, then every D, up to B times (default *3), then dormant ⏸
auq>D an AskUserQuestion has been pending for D; reply keys only agent_tool and spawn
"" disabled

Grammar: 250k is 250 000, 1m is 1 000 000, integer mantissa only; durations carry a unit — 30s, 10m, 1h30m. There is no stop sensor: Stop is the event.

The block shape

A hook is one fenced starlark block under a heading, closed by a ^name anchor. The anchor is the hook's address — keep it stable. declare(...) at load time carries the trigger; def run(event) is the entry and returns the reply.

declare(on = "tokens/200k")

def run(event):
    s = event["session"]
    return context('<context-status tokens="%dk" model="%s" />' % (s["tokens"] // 1000, s["model_display"]))

event is one dict: name, id (the idempotency anchor), input (the official hook input verbatim), session (id, short, dir, agent_file, role, model, tokens, window, percent, cwd, host, herdr, …), sensor (threshold, tokens, fire_seq, budget, idle, pending), facts, time. bash(block = "name") runs a named bash block from the same page. Which reply keys an event accepts: ccc-statusd hooks show --contract.

What your root ships with

The fleet page arrives with the sessions-root template, never from the daemon binary. Its hooks:

Hook Trigger What it does
born-card Join creates your agent card agents/<short-id>/<short-id>.md
agent-brief Join injects the <session-brief> block: session dir, agent home, work folder, worktree instruction, herdr pane
auto-compound UserPromptSubmit injects the chain-stage instructions (ideate → explore → plan → work → review → compound); its presence is the switch
context-status tokens/200k one <context-status …/> line every 200k tokens
continuity-knee tokens>360k,tokens>420k dispatches the ccc-continuity agent — 420k for the 1M-window model class, 360k as the unknown-model floor
idle-default idle>42m*3 an idle nudge; an -adhoc session gets one reminder, a named session is told to continue or report and killme
stop-check-probe, ups-context-probe "" off; diagnostic probes you can enable per session with hook.<name>.on: "<Event>"

The live list, with the mount each hook comes from, its block revision, when it last fired and its state, is one command — run it on your machine rather than trusting this table:

ccc-statusd hooks show

A healthy fresh root prints one ok row per hook above, an off row naming the two probes, and ⚠ none.

Customizing

A change is a put on the right mount: a session-wide tweak on <session-dir>/HOOKS.md, a per-seat tweak on agents/<short-id>/HOOKS.md — a new block, or just an override key. Then ccc-statusd hooks check to validate, ccc-statusd hooks fire <page> <hook> to run one hook dry (the program runs, bash() is stubbed, effects are listed and applied nowhere), and hooks show to see it resolve.

The verbs

ccc-statusd hooks --help — the five:

  • show [session-id] — resolves the plane as a real fire would; --verbose adds mounts, overrides and envelope previews; --contract prints the reply-key table per event.
  • check [page.md] [--file <draft>] [--session S] — validates the plane and prints what it checked. Exit 0 no finding, 1 findings, 2 the plane could not be read; the last line reads verdict: no finding on the HOOKS.md plane (<n> warning(s)). when clean, and when stdout is a pipe a final ccc-statusd hooks check: VERDICT=<n> … line is added, because a pipe masks the exit code.
  • fire <page> <hook> [--event-json F] [--live] — dry by default.
  • init [--root PATH] [--write-mount] — the precondition ladder: sessions-mount → engine-binary → fleet-page → hooks-check → hooks-show, each refusal printing the exact missing command. Exit 0 ready, 1 ran but not ready, 2 a precondition failed.
  • plane — the operator kill switch: stop or resume all hook firing on this host.

Where the detail lives

The installed ccc-base skill, at $UCC_HOME/profiles/<profile>/skills/ccc-base/:

  • hooks-reference.md — the three mounts, override keys, the event envelope, sensors, markers, validation.
  • hooks-events.md — one entry per Claude Code event: input keys, exit-2 rule, reply keys, timeout.

ccc-statusd hook --help documents the transport side, including the env flags (CCC_TOOL_USE_ALLOW_ALL, CCC_USER_PROMPT_SUBMIT_FREQUENCY, CCC_STOP_IGNORE_SYMBOL, CCC_HOOK_LOG_FILE).

MCP server

Technical chapter.mcp-server raw

ccc-statusd is the local daemon every seat on a UCC machine talks to. Besides messaging and lifecycle, it serves a Model Context Protocol (MCP) server over HTTP on localhost. Through it your agent reads, writes and queries the fleet's markdown records — session cards, task boards, memos, wiki pages — with the guards other agents rely on, instead of editing files raw. When a skill says read, put or sql, it means these tools.

flowchart LR
  H[Claude Code seat] -- "HTTP + token" --> D[ccc-statusd
MCP server] D -- "~/MERIDIAN.md" --> R1[(sessions root
primary)] D --> R2[(home-wiki)] D --> R3[(other roots)]

The tools

Eleven tools, one surface. The harness lists them as mcp__ccc-statusd__<name>.

Tool One line
read A record as its section map (toc) or as the sections you name. First call on any record.
put One guarded write: section edits, a frontmatter patch, an attestation pin. Receipt's first word is the verdict (wrote, born, unchanged, skipped, partial).
sql DuckDB over one root's corpus — docs, sections, links, tags, tasks. Board and seat questions.
script A Starlark read-decide-write program, landed as one sealed commit.
run Runs a page's declared task.<name> blocks.
sub What changed under the primary sessions root since a cursor.
roots The bound trees, the primary, and build/daemon/engine freshness. Your orientation call.
resolve Where a ref lands — physical path, addressing kind, root — without reading it.
walk The pin graph up (what a record rests on) or down (who rests on it).
token_count Real token cost of a text, record or section, through a live tokenizer.
worktree op: enter opens a branch and git worktree in your work folder; op: exit releases it. The one worktree door.

Parameter semantics, refusal vocabulary and the loops are the installed ccc-meridian skill (load /ccc-meridian; full detail in its references/mcp-tools.md). The records themselves — cards, rosters, memos, the armed rules that fill and refuse them — are the Meridian tools chapter and Sessions & wiki. The authority on which tools exist is the live server: a roots call, or your harness's own tool list.

Roots and the sessions: alias

The daemon does not see your whole disk. It sees the mount table in ~/MERIDIAN.md — one meridian-mount block per root, each with a name: and a path:. One mount is primary: true and carries alias: sessions; that is the sessions root, the tree the daemon writes and sub watches. A mount carries at most one alias.

Ref grammar for every tool is [root:]path:

  • sessions:agents/<short-id>/<short-id>.md — root-qualified, reaches the named tree.
  • tasks/fix-auth.md — a bare path is ambient: it lands in your own session directory.
  • An absolute path works when it falls under a bound root.
  • A # fragment refuses; sections are addressed by the sections, at or hpath parameters.

roots shows the table as the daemon parsed it; resolve says where one ref would land before you write there. The mrd CLI (mrd read, mrd config get) is the other client of the same engine and reads the same table.

How your seat gets it

Three pieces, all installer-owned; you do not hand-edit an .mcp.json.

  1. The server file — $UCC_HOME/config/mcp/ccc-statusd.md. Frontmatter gates it (requires: ["exec:${HOME}/.local/bin/ccc-statusd", "env:CCC_STATUSD_MCP_TOKEN"]); the body's JSON fence is the server object: type http, url http://localhost:${CCC_STATUSD_MCP_PORT:-19223}/mcp, an Authorization: Bearer header.
  2. The launch page opts in by name — mcpServers: [ccc-statusd, shellkit] in $UCC_HOME/config/launch/default.md. A launch page without that line starts a seat with no record tools.
  3. The token — $UCC_HOME/default-env.sh exports CCC_STATUSD_MCP_PORT (default 19223) and CCC_STATUSD_MCP_TOKEN, reading the token the daemon minted at $UCC_HOME/cache/ccc-status/mcp-token when the variable is unset.

The hazard, in the server file's own words: a launcher whose shell lacks the token "launches an agent with no read/put/sql/sub at all, silently." So the first thing to do in a new seat is call roots. If it answers with the mount table, you are wired. From a shell, curl -s -o /dev/null -w '%{http_code}\n' http://localhost:19223/mcp printing 401 means the daemon is listening and wants the bearer token; a connection refused means the daemon is not running (ccc-statusd status).

Which harnesses have it

  • Claude Code seats — yes. Every UCC launcher that runs the Claude Code harness gets the server, including a gateway profile that points Claude Code at a non-Anthropic model. This is the path to full fleet participation: cards, boards, memos, AskUserQuestion.
  • A native Codex CLI seat — no. It has no ccc-statusd MCP. It can read records with mrd read, and the hooks plane still works (its card and daemon row exist), but it cannot put, write a board, or answer an AskUserQuestion. Details and the workaround are on Codex (limited).

Names you will meet

  • sessions: — the primary root's alias; every skill spells it.
  • ~/MERIDIAN.md — the mount table and this machine's mrd config values.
  • mcp__ccc-statusd__read and friends — the tools as the harness names them.
  • CCC_STATUSD_MCP_PORT, CCC_STATUSD_MCP_TOKEN — the gate.
  • ccc-meridian — the skill that carries the tools' semantics.

Meridian tools

Technical chapter.meridian-tools raw

Meridian is the engine under every session record. It reads a folder of Obsidian-flavoured markdown into a world model, serves byte-exact section reads, and applies guarded writes that either land whole or do not land at all. It ships as one binary, mrd, with two faces over the same engine: the tools your agent calls (read, put, sql, script, walk, resolve, and the pin field on put), and the mrd command line. The MCP server chapter covers how those tools reach your agent; this chapter covers what they do and how a write is shaped.

The one idea to carry away: a write is a plan against something you read. Every read hands you a token naming what you saw. Every write carries that token back. If the world still matches, the write lands; if it moved, the write is refused and the file is byte-untouched. That is how several agents share one tree with no merge step and no lost paragraph.

The shape of every write

flowchart LR
  R["read
toc, then sections"] --> M["tokens
mint · fingerprint · rev"] M --> P["put
if_fingerprint + scope"] P -->|"premise held"| W["receipt
wrote · born"] P -->|"premise moved"| X["refused
nothing landed"] X -. "re-read, re-plan" .-> R

The tools

read — the map before the bytes. A read serves either a record's section map (the toc: heading tree, a word count and a rev per section) or exactly the sections you name. Start with depth: 2 on anything large; the toc prices every section before you pay for one. Selectors are a heading path root-anchored at the file's top heading (Task: fix-auth/Goal, never a bare Goal), a dewey ordinal from the toc's first column (1.4), a block id (^id), or the hpath array copied from a toc row. A sections answer serves at most 20,000 words and 64 selectors — over either it refuses and serves nothing, never truncates; narrow and ask again.

Three lines in a read header matter later:

  • corpus_fingerprint: — the whole root's version.
  • mint {path}: <token> — the version of exactly that file. This is the token you carry into a write.
  • fingerprint:fp1.… on each section of a sections read — the proof a pin needs.

put — every write, one guarded call. One record, one call: a section edit (a single op, or a batch in edits[]), a frontmatter properties patch, and a pin, in any combination. ref plus body on a path that does not exist is a birth; the same call on an existing record refuses. Four ops: replace (an exact find anchor that matches once), append, replace_section (needs a fresh section rev), create_section. A properties patch touches only the keys you name and never the body.

The receipt's first word is the verdict: wrote, born, unchanged, skipped, partial. The last two are not errors — they name edits whose bytes already landed within the ten-minute replay window. Verify; do not resend.

pin — attestation, a field on put. A pin records in this record that it draws from a section of another record, at that section's content fingerprint. It needs proof of read: the fp1.… token from a sections read you made in this session, copied untrimmed. A pin also mints an anchor in the target, so the target's file_rev moves while its content fingerprint does not — that is not drift.

sql — the corpus as a table. One DuckDB statement over one root: doc, section, body, link, backlink, tag, frontmatter, record, task. Read-only in effect — a mutation is visible to your own statement and rolled back when the call ends. The full result is written to a TSV named on the answer's last line; the inline answer shows the first rows. The freshness banner's as_of_fingerprint is a legal if_fingerprint for a following write.

script — read, decide, write, as one commit. A Starlark program with three calls: read(path[, section=…]), put(path, props={…}) or put(path, section=…, append=…), and me(). Everything it writes seals as one commit — one fingerprint advance, one receipt naming every file — or nothing lands. dry: true rehearses without applying. This is the door for carrying content from one record into another atomically.

walk and resolve. walk follows a page's pin graph — up for what it rests on, down: true for who rests on it — coloured green (verified), red (drifted), grey (unverifiable). It lists no frontmatter edge: spawned-by, handoff-from are sql over frontmatter, so an empty walk is not "no successors". resolve answers where a ref would land — the physical path, the addressing kind (rooted, ambient, absolute) and the serving root — without reading it.

mrd — the same engine from a shell. mrd read <ref>, mrd sql "<statement>" --root <name>, mrd resolve, mrd script. A ! in its help gutter marks a verb that changes something; everything else is a read. mrd --help is the authority on its verbs.

The tokens and their grain

Token Grain Where you get it What a write carrying it says
section rev one section a toc row or a sections header "this section reads as I saw it"
file_rev one file a read header, every put receipt the document's version
mint token + scope one file (or folder) the mint {path}: line of a read "I planned against this file; I do not care what happened elsewhere"
corpus_fingerprint the whole root a read header, a put receipt, sql's as_of_fingerprint, a script's entry_fingerprint "I planned against the world; tell me if anything moved"

Guard one file, pay one file; guard the world, pay the world. In a busy tree the world token refuses constantly, because someone is always writing somewhere; the mint token paired with scope is the everyday instrument. A batch edits[] requires if_fingerprint — the plan's premise rides with the plan.

Refusals are the feature

A refusal is checked before anything commits, so there is never a half-written file to clean up. Each one names the fact and the recovery:

Refusal The fact it states What you do
fingerprint_mismatch the premise moved — the root, or the scoped file, is not what your read showed re-read, re-plan the write against what the read shows
cas_mismatch the section changed since your read; the answer prints the current bytes re-apply your edit over the printed content
write_conflict a pin's sec_rev is stale re-read that section
pin_proof_required the pin carries no fingerprint from a read in this session read the section, copy the fp1.… token into the pin
pin_target_missing the section you named does not exist in the target list the target's sections with a toc read
a door's refusal with teaching a session rule declined the bytes — a card outside your board, a status outside the seven columns, a memo missing its type read the teaching; it names the missing thing

The one law: never resend the same bytes. The engine says it in the refusal itself — "never resend it under the same premise, which refuses identically". A retry after a fresh read is, by construction, a new plan.

Day one with records

Your agent does three things with records on its first working day. All three are put against a read.

Read your card. Your card is sessions:agents/<short-id>/<short-id>.md; sessions: is the alias every host declares for its sessions root, and the short id is in the banner your agent sees at boot.

read ref="sessions:agents/<short-id>/<short-id>.md" depth=1

The header line is the file's absolute path; props: are its frontmatter keys — role, session, seat, status.

Claim a card. Read the task card first (the same call, on …/tasks/<slug>.md); take the mint {…}: token and the brace path from the answer; hand both back:

put ref="sessions:year=<YYYY>/month=<MM>/<session>/tasks/<slug>.md"
    properties={"owner": "[[<short-id>]]", "status": "In Progress"}
    if_fingerprint="<the mint token>"
    scope="year=<YYYY>/month=<MM>/<session>/tasks/<slug>.md"

wrote … props: owner=[[<short-id>]] status=In Progress is the receipt. If a teammate claimed it between your read and your put, the answer is fingerprint_mismatch and the card still says what it said — read it again and you will see their name.

Write a memo. A finding worth keeping past the conversation is one born file in your agent home. Supply the meaning; the door stamps the rest.

put ref="sessions:agents/<short-id>/memos/<slug>.md"
    body="---\ntype: memo\ntags: [memo/gotcha]\n---\n# <slug in words>\n\n<the finding, one per file>\n"

born … is the receipt. A taken slug refuses at the door — pick another.

Try it on your own card

Two calls the reader can run at once, with nothing at risk. First read your card's Notes section (a card has four top-level headings, so the section is addressed by its hpath, not by a bare name):

read ref="sessions:agents/<short-id>/<short-id>.md" sections=[[{"h":"Notes"}]]

The answer opens with a header carrying mint {agents/<short-id>/<short-id>.md}: <token> and a == Notes (rev:… words:…) fingerprint:fp1.… == line. Then append one line under Notes, guarded by that mint token and its brace path:

put ref="sessions:agents/<short-id>/<short-id>.md"
    op="append" at="Notes" body="- first guarded write: landed\n"
    if_fingerprint="<the mint token>"
    scope="agents/<short-id>/<short-id>.md"

The receipt reads wrote … §Notes rev:<old>→<new> file_rev:… corpus_fingerprint:…. Send the identical call a second time and the receipt reads skipped with its explanation — the bytes are already there. That pair of receipts is the whole contract in miniature.

Names you will meet

  • sessions: — the root alias; resolve ref="sessions:<path>" shows the physical directory it names.
  • agents/<short-id>/<short-id>.md — your card; agents/<short-id>/memos/ — your memos.
  • year=YYYY/month=MM/<DD-HH-slug>/ — a session; INTENT.md inside it is what makes it one; tasks/, rosters/, results/, decisions/ appear when something is first born in them.
  • MERIDIAN.md — the file that declares a folder as a root; mrd resolve says which one answers for a path.
  • HOOKS.md — where the session rules (the "doors") are armed; their refusals arrive through put with teaching.

Where the detail lives

The installed ccc-meridian skill is the authority: SKILL.md for the tool surface and the addressing grammar, references/mcp-tools.md for every parameter and the full refusal vocabulary, references/sql.md for the table schema, references/cli.md for mrd. The session tree's own conventions — cards, boards, memos, the seven columns — are the ccc-base skill. The Sessions chapter of this handbook covers what those records mean; this one covers how they are read and written.

UCC folders

Technical chapter.ucc-folders raw

UCC is one directory. Everything the installer put on your machine — every Claude Code home, the shared skills, the launchers, the daemon's data — lives under $UCC_HOME, which defaults to ~/.local/share/ucc. Every ucc-* command starts there. Print yours:

echo "$UCC_HOME"

This page tells you which folder holds what, which files are yours to edit, and which the installer rewrites on every update.

The tree

flowchart LR
  H["$UCC_HOME"] --> P["profiles/"]
  H --> S["shared/"]
  H --> B["bin/"]
  H --> C["config/"]
  H --> D["data/"]
  H --> W["work/"]
  H --> CA["cache/"]
  H --> SR["sessions/"]
  H --> U["user-env.sh"]
  S --> SK["skills/"]
  S --> SA["agents/"]
  S --> SP["projects/"]
  S --> SC["CLAUDE.md"]
  P --> PN["/"]
  PN -.-> SK
  PN -.-> SA
  PN -.-> SC
  X["~/.codex/skills/ucc"] -.-> SK

Dotted lines are symlinks: each profile's skills, agents and CLAUDE.md point into shared/, and ~/.codex/skills/ucc points at the same shared/skills/ tree, so Codex reads the skills Claude Code reads.

Folder by folder

Path What it holds Who writes it
profiles/<name>/ One complete Claude Code home per profile: .claude.json, .credentials.json, settings.json, and symlinks skills, agents, CLAUDE.md, projects into shared/. ls "$UCC_HOME/profiles" lists the names; each has a launcher ucc-<name> in bin/. The installer creates it; Claude Code writes its own state into it.
shared/skills/ The one skills tree — one directory per skill, each with a SKILL.md (ccc-base, ccc-work, llm-wiki, …). Every profile's skills and ~/.codex/skills/ucc point here. The installer. A skill you write for yourself goes in your own repo and is linked in; a hand edit here is lost on the next update.
shared/agents/ Subagent definitions. Every profile's agents points here. The installer.
shared/projects/ Claude Code's transcripts: one folder per project directory, one <session-uuid>.jsonl per session. The folder name is the project path with every character that is not a letter or digit replaced by -, so /Users/you/repo is -Users-you-repo. Claude Code itself. Read it; never edit it.
shared/CLAUDE.md Your global instructions. Every profile's CLAUDE.md is a symlink to this one file, so an edit here reaches every profile. You.
bin/ The launchers: ucc-<profile> for each profile, ucc-auto (picks a profile for you), ucc-cli (the maintenance tool), ucc-codexd (the local proxy behind the ucc-codex launchers), ucc-source-<profile> (prints one profile's environment for a shell to source), and skills-bin/ (commands that skills put on your PATH). ls "$UCC_HOME"/bin/ucc-* lists them; ignore the *-cli, *-ip and ucc-source-* entries. The installer.
config/ The launch plane — what every ucc-* launch gets as system prompt, model, tools and MCP servers. launch/*.md is one agent definition per launch kind (the page body is the system prompt), mcp/*.md one MCP server each, fleet.md the fleet-wide tool deny list. GUIDE.md in this folder is the human-facing authority. The installer, unless the folder is a git checkout (config/.git exists) — then the checkout wins. ucc-cli config update refreshes it alone.
data/ State the daemons keep: ccc-pages/ (published pages, keys), ccc-telegram/. The daemons. Never edit by hand.
cache/ Working state the daemons rebuild: ccc-status/ holds the ccc-statusd daemon's state and log. The daemons. Never edit by hand.
sessions/ The sessions root a fresh install seeds here, unless CCC_SESSIONS_ROOT points elsewhere — the records your agents write. See Sessions & wiki. Your agents, through the daemon.
work/<short-id>/ Each agent's private working folder — checkouts, builds, captures. The agent's join brief names it. Each agent, for itself.
user-env.sh Your user-scope environment, sourced by every launcher and by ccc-statusd start. The installer writes UCC_AUTH_TOKEN, UCC_USERNAME and UCC_R2_BASE_URL here; you add ENCRYPTION_PASSWORD and the sync settings yourself. The installer, then you.
ucc-init.sh The shell setup your shell's startup file sources: puts bin/ and bin/skills-bin/ on your PATH and defines the ucc-* shell functions. The installer.
default-env.sh, user-override.sh The installer's base environment, and a last override layer. A launcher sources default-env.sh, then user-env.sh, then user-override.sh, in that order. Installer / you.

Two things a reader expects here that work differently:

  • There is no hooks.toml in $UCC_HOME. Hooks live on the sessions root's HOOKS.md; see Hooks. ccc-statusd hooks show prints what is armed.
  • The sessions root is its own tree. A fresh install seeds it at $UCC_HOME/sessions with its own git history; CCC_SESSIONS_ROOT can place it anywhere. The rest of $UCC_HOME holds the tooling, the sessions root holds the records.

What you may edit

Yours, and safe across updates:

  • user-env.sh — environment variables and secrets for every launch.
  • user-override.sh — the last word when you need to override something the installer set.
  • shared/CLAUDE.md — your global instructions.
  • work/<short-id>/ — your agents' working folders.

Read, but do not edit by hand: shared/skills/, shared/agents/, bin/, config/, default-env.sh, profiles/<name>/settings.json, and everything under data/ and shared/projects/. The installer rewrites these; ucc-cli update (needs ENCRYPTION_PASSWORD in your environment) re-runs the installer and brings the whole tree to the latest release, ucc-cli config update refreshes config/ alone, and ucc-cli repair checks and fixes the profile symlinks.

If something looks wrong after an update, the check with zero writes is:

"$UCC_HOME/shared/cc-continuity/install.sh" --check

It writes nothing. A healthy tree prints one line starting cc-continuity OK: with the skill and agent counts; a broken one prints one FAIL line per broken surface.

Where the detail lives

  • config/GUIDE.md — how the launch plane composes a system prompt, and the two refusals that fail a launch (--launch NAME with no page; an explicit --model that differs from a profile's pinned model).
  • Skill ccc-base, file setup.md — install, the preflight checks, and --fix.
  • ucc-cli --help — every maintenance command; ucc-cli <command> --help for one.

What you have

Technical chapter.what-you-have raw

After the install, your computer runs an agent you talk to in a terminal window. Around it sit a few pieces that let it remember, follow house rules, keep its notes in order and build web pages. You never operate these pieces by hand — you ask the agent, and it uses them. The picture shows how they connect; each line below says what one piece is and where to read more.

flowchart LR
  Y[you] --> A[your agent]
  A --> K[skills]
  A --> D[a helper
always on] D --> R[(notes and
records)] A --> P[web pages]
  • Agents and skills — one folder on your computer holds the agents you can start and the skills that teach them how to work. UCC folders
  • A notebook that is backed up — every piece of work gets a folder of notes, and what is worth keeping graduates into a wiki of your own. Sessions & wiki
  • House rules — small programs that fire at set moments, such as a reminder when an agent has sat idle too long. Hooks
  • A helper that keeps the notes — a program always running in the background, through which your agent reads and writes those notes. MCP server
  • Edits that never collide — several agents can write the same notes at once, and a write that would overwrite someone else's is refused instead of lost. Meridian tools
  • Web pages with a memory — your agent publishes pages that anyone with the link can open, and updates them without rebuilding. ccc-pages

When an agent needs an answer from you, it can reach you on Telegram.

The four technical chapters follow, closed. Open one when you want the folder names, commands and file formats behind a line above.