ai-agent-hub-template · guide

Agent hub guide

How to make, run and configure an agent hub: a private repo of Bash and Markdown that holds the context, state and history of one technical persona's work for one client, on Claude Code. Every session starts with what is live, open and waiting. The agents keep it up to date. The client's code stays in its own repos.

New to it? Start with the 15-minute hands-on training.

What a hub is

A hub is one private Git repo per client and role. Its CLAUDE.md makes Claude Code the persona you picked, such as a DevOps/SRE, a staff engineer or a fractional CTO, and its folders hold the state of each work area. Hooks print that state when a session starts and tell each terminal what the others changed. When a folder session ends, they journal its commits and commit its notes. Scheduled jobs push the hub, run your monitors and write a morning briefing.

Context

The persona, the shared rules, and each folder's CLAUDE.md and ACCESS.md (names and locations, never values).

State

Each folder's STATE.md: Live, Staged, Open, Waiting on, Next session starts with.

History

journal/<UTC date>.md, the append-only timeline every agent and hook writes to, plus memory/.

Recall

bin/brain: every Markdown section in a local SQLite index, searched by keyword and by meaning.

flowchart LR
  subgraph HUB["Hub repo (private, Git)"]
    direction TB
    CM["CLAUDE.md + persona agent"]
    F["projects/#lt;x#gt; · ops/#lt;x#gt;
CLAUDE · ACCESS · STATE · memory/"] J["journal/#lt;UTC date#gt;.md"] HC["hub.conf
(names and locations only;
commit it once by hand)"] BIN["bin/ scripts + hooks"] end subgraph MAC["This Mac (not in the repo)"] direction TB CC["Claude config dir
projects/…/memory → hub memory
skills/bmad-* → hub skills"] ST["~/.agent-hub/#lt;name#gt;/
logs, locks, snapshots, monitor state"] KC["macOS Keychain
(service = hub name)"] LD["launchd jobs
briefing · monitor · sync"] DB[".local/brain.db
(gitignored)"] OL["Ollama
(local embeddings)"] end CODE["Client code repos
(own checkouts)"] GH["GitHub remote
(the hub's origin)"] BIN -- "bin/setup links" --> CC BIN --> ST BIN -- "bin/secret" --> KC BIN -- "bin/schedule" --> LD BIN -- "bin/brain" --> DB DB -. vectors .- OL F -- "ACCESS.md Code table" --> CODE HUB -- "bin/sync" --> GH

Where everything lives. The hub repo travels between Macs. Secrets, logs, the search index and links into Claude stay on each Mac.

Requirements

The scripts target macOS and Bash 3.2 or later. They use launchd, the Keychain (security), notifications and Terminal or iTerm through osascript, and BSD stat and date.

ToolUsed forNeeded?
claudeClaude Code: every session, the briefingYes
git, python3the hub itself; bin/init, bin/work, bin/changes, bin/brainYes
ghcreating the hub from the template, cloning client repos, checking repo accessYes, for GitHub use
gitleaksthe secret check. Without it the pre-commit hook only warns, so hand and session-end commits go in unscanned, and bin/sync --auto refuses to runYes
ssh, curl, nodebin/remote, the staging check, and tools the agents may need. bin/doctor checks for all threeChecked
Ollama + nomic-embed-textmeaning-based search in bin/brainOptional (keyword search works without it)
timeout / gtimeoutmonitor and index time limitsOptional (a built-in fallback is used)

Add more tools for bin/doctor to check with HUB_TOOLS="kubectl terraform" in hub.conf.

Make a hub

flowchart TD
  A["Create a private repo from the template
gh repo create … --template"] --> B["bin/init --persona … --client … --owner …"] B --> B1["writes hub.conf"] B --> B2["renders CLAUDE.md, ACCESS.md, README.md
and .claude/agents/#lt;persona#gt;.md"] B --> B3["bin/new for each folder:
CLAUDE · ACCESS · STATE · memory · settings · agent"] B --> B4["git init (only if no .git yet),
hooks path .githooks, first journal entry"] B1 & B2 & B3 & B4 --> K["commit and push once by hand
(bin/init commits nothing; bin/sync never stages hub.conf)"] K --> C["bin/setup
link memory + BMAD skills into the Claude config dir"] C --> D["ollama pull nomic-embed-text
bin/brain index"] D --> E["bin/doctor until what you need is OK
bin/secret --set #lt;name#gt; for each secret"] E --> F["bin/schedule install
briefing · monitor · sync"] F --> G["claude (or your --claude wrapper)
at the hub root"]

From template to first session.

  1. Create the repo from the template and clone it.
    gh repo create my-acme-sre --private --template alvin-reyes/ai-agent-hub-template --clone
    cd my-acme-sre

    Without GitHub: run bin/init … --dest ~/Project/acme-sre from a template checkout. It copies the template to that new folder first, leaving out .git, hub.conf, repos/ and _bmad-output/. The folder must not exist yet. Add a private remote there (git remote add origin <url>). Without an origin, sync commits but never pushes, and still logs synced.

  2. Pick a persona and initialize.
    bin/init --list                                   # the personas
    bin/init --persona devops-sre --client "Acme Corp" --owner "Sam Lee" \
      --repo acme/infra=~/code/infra --remote deploy@staging.acme.com:22 --remote-key ~/.ssh/acme \
      --staging-url https://staging.acme.com --secret grafana-token --briefing 08:00

    --persona, --client and --owner are required. bin/init never overwrites a hub that already has a hub.conf, and it writes no secrets.

  3. Commit and push the hub once, by hand.
    git add -A && git commit -m "Initialize the hub" && git push -u origin main

    bin/init commits nothing, and bin/sync's path list leaves out hub.conf. Without this commit, a fresh clone has no hub.conf: its hub name falls back to the folder name, which changes the state folder, launchd labels and Keychain service, and it has no secret names to check. Pushing with -u also sets the upstream that auto sync needs before it can scan commits ahead of a push. Commit hub.conf by hand again whenever you edit it.

  4. Link it into Claude and check what this Mac can reach.
    bin/setup                          # memory + skills links, git hooks
    ollama pull nomic-embed-text       # once per Mac, optional
    bin/brain index
    bin/doctor                         # OK / MISSING per tool, login, repo, box, secret
    bin/secret --set grafana-token     # hidden prompt; never an argument
  5. Turn on the scheduled jobs.
    bin/schedule install               # weekday briefing, monitor + sync every 30 min
  6. Start working.
    claude                             # at the hub root: the main agent (use your --claude wrapper if you set one)
    bin/work ops/environments          # deep work in one folder's repo, in a new Terminal

bin/init options

OptionSetsDefault
--persona <slug>the role (a file in personas/); --list shows themrequired
--client, --ownerwho the work is for, and who the agent works forrequired
--namethe hub name (a-z, 0-9, -), also the Keychain service and launchd label<client slug>-<persona>
--dest <dir>copy the template to a new folder firstinitialize in place
--folders "projects/a ops/b"the work areas, each projects/<name> or ops/<name>the persona's
--repo owner/name[=checkout]code repos (repeatable); rows go into the first folder's Code table, where deep work happensnone
--code-owner <org>the GitHub owner for repos named without onenone
--remote user@host[:port]the box bin/remote reaches over SSHnone (port 22)
--remote-key, --remote-dirthe SSH key path, and the directory commands run innone
--staging-urla URL bin/doctor checks (2xx, 3xx or 401 is OK)none
--secret <name>secret names bin/doctor checks for in the Keychain (repeatable; a-z, 0-9, -)none
--briefing HH:MMthe weekday morning-briefing time, this Mac's local time08:00
--claude <cmd>the Claude install to run: claude, or a wrapper that sets its own CLAUDE_CONFIG_DIRclaude
--claude-config-dir <dir>that install's config dir, where memory and skills are linkedread from the wrapper's export CLAUDE_CONFIG_DIR=, else ~/.claude
--model M, --effort Edeep-work sessions; the model also seeds the persona agent at init; effort is low|medium|high|xhigh|max and applies to bin/work onlythe persona's model
--agent-model Mthe folder agents' model (written when each is made) and bin/work's specialist agentssonnet
--briefing-model Mthe morning briefinghaiku
--brain-path <path>extra folders, files or globs the brain indexes (repeatable; quote globs)none
--embed-model M, --ollama-url URLthe local embedding model and Ollama addressnomic-embed-text, http://localhost:11434
--auto-sync on|offHUB_AUTO_SYNC: push every 30 minutes and at each folder session's endon

bin/init fills {{CLIENT}} and {{OWNER}} in the persona text. It sets BMAD's project_name and output_folder in _bmad/config.toml and creates memory/MEMORY.md. It runs git init on main when there is no .git yet, sets core.hooksPath .githooks, makes every folder with bin/new and journals "Hub initialized from ai-agent-hub-template".

Personas

SlugRoleDefault foldersModel
data-engineerData Engineerprojects/pipelines, projects/warehouse, projects/reporting, ops/data-qualitysonnet
devops-sreDevOps / Site Reliability Engineerops/environments, ops/ci-cd, ops/monitoring, ops/securitysonnet
engineering-managerEngineering Managerprojects/team, projects/delivery, projects/clientsonnet
fractional-ctoFractional CTOprojects/strategy, projects/proposals, projects/vendors, projects/clientopus
frontend-engineerFrontend Engineerprojects/web, projects/design-system, projects/clientsonnet
ml-ai-engineerML / AI Engineerprojects/models, projects/agents, projects/evals, projects/proposalsopus
mobile-engineerMobile Engineerprojects/ios, projects/android, projects/releasessonnet
qa-test-engineerQA / Test Engineerprojects/app, projects/test-plans, ops/environmentssonnet
security-engineerSecurity Engineerops/security, ops/access, projects/app, projects/reviewsopus
solutions-architectSolutions Architectprojects/architecture, projects/proposals, projects/clientopus
staff-software-engineerStaff Software Engineerprojects/app, projects/proposals, projects/client, ops/environmentsopus
tech-leadTech Leadprojects/app, projects/planning, projects/clientsonnet
technical-product-managerTechnical Product Managerprojects/roadmap, projects/specs, projects/clientsonnet

Add a persona

Copy a file in personas/. Keep its front matter and use {{CLIENT}} and {{OWNER}} in the body, then write the persona's habits:

---
title: DevOps / Site Reliability Engineer
folders: ops/environments ops/ci-cd ops/monitoring ops/security
model: sonnet
briefing_focus: Report failing checks, expiring certificates, backups not verified, and cost changes.
---

## Who you are

You are the **DevOps / Site Reliability Engineer** for {{CLIENT}}, working for {{OWNER}}. …

briefing_focus is added to the morning briefing's prompt. model becomes the hub's default session model.

What's inside a hub

my-acme-sre/
├── CLAUDE.md              # the persona + shared rules, git boundaries, token budget (rendered)
├── ACCESS.md              # shared access: code, remote box, services, credential LOCATIONS, permissions
├── README.md              # this hub's commands and folders (rendered)
├── hub.conf               # who the agent is and what it can reach (names only)
├── .claude/
│   ├── settings.json      # hub-root hooks
│   ├── agents/<persona>.md # the persona agent
│   ├── agents/<folder>.md  # one named agent per folder (ops/x → ops-x)
│   └── skills/bmad-*      # BMAD v6 skills
├── projects/<name>/  ·  ops/<name>/
│   ├── CLAUDE.md          # what this work is; imports ACCESS, memory index, STATE
│   ├── ACCESS.md          # Code, Machines, Services, Credentials, Permissions, Undo
│   ├── STATE.md           # Live · Staged · Open · Waiting on · Next session starts with
│   ├── memory/MEMORY.md   # the folder's memory index
│   ├── monitors.d/*.sh    # optional read-only checks
│   ├── .claude/agents/*.md # optional specialist agents, passed by bin/work --agents
│   └── .claude/settings.json # the folder's hooks (used by bin/work)
├── memory/MEMORY.md       # hub-wide facts, people, gotchas
├── journal/YYYY-MM-DD.md  # one file per UTC day, append only
├── bin/                   # the scripts
├── templates/  personas/  launchd/
├── .ade/  _bmad/  _bmad-output/   # methodology rules, BMAD config and output
├── .githooks/pre-commit   # refuses repos/, files over 1 MB, secrets
└── .local/brain.db        # the search index (gitignored)

A folder's files

FileHolds
CLAUDE.mdTwo lines on what the work is, plus folder rules: code changes only in this folder's repos, on a branch, as a draft PR; update STATE and the journal at the end. It imports @ACCESS.md, @memory/MEMORY.md and @STATE.md.
ACCESS.mdCode table (Repo · Checkout · Default branch · Who merges · CI), Machines, Services, Credentials (locations only), Permissions and Undo (live change → rollback command). The Code table's Checkout column is what bin/work, bin/setup and the hooks read.
STATE.mdUpdated: then Live, Staged, Open, Waiting on, Next session starts with. bin/context prints the first lines of Live, Open, Waiting on and Next session.
memory/One fact per file with a description: in its front matter, indexed in MEMORY.md. Never secrets, phone numbers or contact email addresses.

Make a new work area with bin/new projects/<name> or bin/new ops/<name>. It never overwrites existing files (it prints kept), and it runs bin/setup to link the new folder's memory.

A journal entry looks like this:

## 09:42 UTC · ops/environments
Rotated the staging TLS cert; expires 2027-02-01. Waiting on the client for the prod window.

Daily work

flowchart TD
  BR["08:00 weekday · bin/briefing
read-only summary → journal + notification"] --> S S["claude at the hub root
SessionStart prints every folder's state + recent journal"] --> P["Main agent proposes the day's tasks:
waiting on you, due, broken"] P --> OK{"Your go?"} OK -- "a folder task" --> AG["Spawn that folder's agent
(.claude/agents/#lt;folder#gt;.md)
it loads its own context"] OK -- "deep work" --> W["bin/work #lt;folder#gt;
new Terminal in the repo checkout"] OK -- "cross-folder judgement" --> PA["Persona agent
(.claude/agents/#lt;persona#gt;.md)"] AG & PA --> U["Agent updates STATE.md
+ bin/journal #lt;folder#gt; …"] W --> U2["Stop hook asks for a STATE update after new commits
SessionEnd journals commits, commits folder, starts sync"] U & U2 --> R["Main agent confirms STATE + journal,
reports to you in a few lines"] R --> SY["bin/sync --auto (every 30 min and each session end)
pushes the hub"]

A day in the hub. Hub commits come from the main agent, bin/sync, and a folder session's end (that folder and the journal only). Code is committed only in the client repos.

  • Main agent (a session at the hub root, started with the hub's Claude install) reads the state and proposes tasks. On your go, it spawns each folder's named agent with the task, not the history. It never does a folder's work without reading that folder's CLAUDE.md, ACCESS.md and STATE.md first.
  • Folder agents (projects/app → app, ops/ci-cd → ops-ci-cd) work only in their folder's repos. They stage production changes for you and end with five lines or fewer.
  • The persona agent handles work that spans folders or needs judgement: planning, trade-offs, proposals, client emails, reviews and deploy decisions.
  • Several terminals can run at once. The UserPromptSubmit hook tells each one what the others changed since its last prompt.

Hooks

The hub root's hooks are in .claude/settings.json. Each folder's hooks are in <folder>/.claude/settings.json, made from templates/folder-settings.json. bin/work passes that file with --settings, because a deep-work session runs in the client's checkout, outside the hub.

sequenceDiagram
  autonumber
  participant You
  participant S as Folder session (bin/work)
  participant H as Hub scripts
  participant R as Client repo
  You->>S: bin/work ops/environments
  S->>H: SessionStart · bin/context #lt;folder#gt; --rules
  H-->>S: hub rules + Tools + hub memory index, STATE.md, last 5 journal entries
  loop every prompt
    You->>S: prompt
    S->>H: UserPromptSubmit · bin/changes #lt;folder#gt;
    H-->>S: other folders' STATE updates, new journal lines, new or updated memory
  end
  S->>R: commits on a branch
  S->>H: Stop · bin/stop-check #lt;folder#gt;
  H-->>S: block once per new commit: "update STATE.md and the journal"
  S->>H: PreCompact · bin/journal (note what was in progress)
  You->>S: exit
  S->>H: SessionEnd · bin/session-end #lt;folder#gt;
  H->>H: journal the session's commits and pushed branches
  H->>H: bin/brain index (≤120 s), commit folder + journal
  H->>H: start bin/sync --auto in the background
  
EventHub rootFolder session
SessionStartbin/context: every folder's Live, Open, Waiting on and Next, flagged "state may be stale" when a repo has commits newer than STATE.md, plus the last 8 journal entriesbin/context <folder> --rules: the hub's Shared rules, Tools, Git boundaries and Token budget, plus the hub memory index, the folder's STATE.md and its last 5 journal entries
UserPromptSubmitbin/changesbin/changes <folder>; also shows hub-root journal entries. Its own folder isn't echoed back
PreCompactjournals a reminder to update the folders' STATE.mdjournals that STATE.md and the entry should say what was in progress
Stopnonebin/stop-check: blocks once per new commit when a repo is ahead of STATE.md; without repos, a reminder if STATE.md wasn't updated today (UTC)
SessionEndnonebin/session-end: journals commits since the session started (first transcript line, else the last 8 hours), refreshes the brain, commits the folder and the journal, and starts bin/sync --auto

bin/changes keeps one snapshot per session in ~/.agent-hub/<name>/changes-seen.<session_id>. The first prompt only records a baseline. Output is capped at 40 lines, journal text is trimmed to 200 characters, and snapshots older than 7 days are removed.

Deep work: bin/work

bin/work [--claude CMD] [--model M] [--effort low|medium|high|xhigh|max] [--prompt TEXT] [--no-remote]
         [--agent-model [name=]M] [--agent-effort [name=]E] <projects|ops>/<name> [repo]
flowchart TD
  A["bin/work #lt;folder#gt; [repo]"] --> B{"Code table rows
in ACCESS.md?"} B -- "none, or a (none …) row" --> H["work in the hub folder itself"] B -- yes --> C["pick [repo] by owner/name or name,
else the first row"] C -- "no match" --> X1["error: no repo #lt;x#gt; · exit 2"] C --> O{"Checkout empty and row says
checkout owned by #lt;folder#gt;?"} O -- yes --> X2["error: run bin/work #lt;that folder#gt;"] O -- no --> D{"checkout exists?"} D -- yes --> E["use it"] D -- "no" --> F["use ~/Project/#lt;name#gt; (AGENT_CODE_DIR),
cloning it with gh if missing;
record the path in ACCESS.md"] E & F & H --> G["build: cd dir, AGENT_FOLDER=…, claude --settings #lt;folder#gt;/.claude/settings.json
--agents (the folder's specialist agents, if any) --model --effort
--remote-control #lt;folder name#gt; -- #lt;prompt#gt;"] G --> T["open a new Terminal (or iTerm) window"]
  • Remote Control is on for every session, named after the folder (projects/acme → acme), so you can follow it from your phone. --no-remote turns it off. Use that with API-key wrappers such as OpenRouter or Ollama, because Remote Control needs a claude.ai login.
  • --prompt "<task>" sends the session's first message.
  • Specialist agents. Markdown agent files in <folder>/.claude/agents/ (front matter name, description, optional tools and model) are passed to the session as --agents. bin/new doesn't create any; add them yourself. HUB_AGENT_MODEL is their default model. --agent-model sonnet or --agent-effort high sets all of them, and --agent-model reviewer=opus sets one; the named value wins. With no specialist agents, these options exit with "has no specialist agents". The folder's named agent in the hub's .claude/agents/ is for the main agent at the hub root, not for bin/work.
  • A repo row marked checkout owned by <folder> with an empty Checkout sends you to that folder instead.
  • WORK_DRY=1 prints the command without opening a window. AGENT_TERMINAL=iterm uses iTerm.

The brain: searchable memory

bin/brain keeps every Markdown and text section of the hub in one SQLite file, .local/brain.db. That covers STATE, memory, the journal, notes, docs and the paths in HUB_BRAIN_PATHS. The file is gitignored, and there is one per hub, so clients never mix. Files stay the source of truth, and the index is rebuilt from them.

ollama pull nomic-embed-text                               # once per Mac
bin/brain index                                            # incremental; --full rebuilds
bin/brain search "what did the client decide about SMS"    # file:line, heading, snippet
bin/brain search -n 15 --json "cert expiry"
bin/brain status                                           # files, sections, embedded, stale, model, db
flowchart LR
  subgraph IDX["bin/brain index"]
    direction TB
    S1["walk the hub + HUB_BRAIN_PATHS
.md and .txt"] --> S2["skip unchanged files (hash)"] S2 --> S3["split at headings;
long sections at blank lines (≤2000 chars)"] S3 --> S4["drop any section that looks like a secret"] S4 --> S5["FTS5 row + Ollama vector
(batches of 32)"] end subgraph Q["bin/brain search"] direction TB Q1["keyword: FTS5 bm25, top 50"] --> RR["rank fusion
score += 1 / (60 + rank)"] Q2["meaning: cosine vs query vector, top 50"] --> RR RR --> OUT["top 8 (or -n N):
path:line · heading · snippet"] end S5 --> Q1 S5 --> Q2
  • Without Ollama, indexing and search still work, by keyword only. Vectors are added on a later run once the model is reachable.
  • Not indexed: .git, .claude, _bmad, _bmad-output, repos, node_modules, .local, templates, personas, tests, test, launchd, dist, build, runs, vendor, target and every hidden folder. The same list applies inside HUB_BRAIN_PATHS folders, so a client test/ or build/ folder there is skipped too.
  • A memory file's name and description title its first section.
  • A folder session's end re-indexes (the hub root has no SessionEnd hook), and bin/doctor reports a stale index or a missing embedding model.

Auto sync

bin/sync commits and pushes hub files only: README.md CLAUDE.md ACCESS.md memory journal reports projects ops docs bin templates tests .claude .githooks .github .gitignore .bigfiles-allow launchd .ade _bmad _bmad-output llms.txt. It never commits repos/ or other staged files.

ModeDoes
bin/syncReview only: lists pending changes and a diff stat, and notes untracked files under .claude/ that --yes would include.
bin/sync --yesCommits sync <date> <time> (through the pre-commit hook) and pushes.
bin/sync --autoThe same, unattended, with the safeguards below. It runs every 30 minutes and at each folder session's end. Exit 0 means synced, nothing to do or skipped. Exit 1 means refused or failed.
flowchart TD
  A["bin/sync --auto"] --> B{"HUB_AUTO_SYNC=0?"}
  B -- yes --> X1["log: skipped (off) · exit 0"]
  B -- no --> C{"lock free?
(stale: owner gone or #gt;2 h old)"} C -- no --> X2["log: another sync is running · exit 0"] C -- yes --> D{"git busy?
index.lock, merge, rebase,
cherry-pick, revert"} D -- yes --> X3["log: skipped: git is busy · exit 0"] D -- no --> E{"gitleaks installed and
hooks path = .githooks?"} E -- no --> X4["log: refused · exit 1"] E -- yes --> F["stage the hub paths"] F --> G{"staged hub files, or
commits ahead of upstream?"} G -- no --> X5["log: nothing to sync · exit 0"] G -- yes --> CM{"commit the hub files only
(pre-commit: repos/, #gt;1 MB, gitleaks)"} CM -- refused --> X8["unstage · log: failed: commit refused · exit 1"] CM -- "ok (or nothing new to commit)" --> OR{"origin remote?"} OR -- no --> X9["log: synced (local commit only, nothing pushed)"] OR -- yes --> H{"upstream set and
commits ahead?"} H -- yes --> SC["gitleaks scan of every commit to push"] SC -- "secret found" --> X6["log: refused, nothing pushed · exit 1"] SC -- clean --> I{"git push"} H -- "no upstream" --> I I -- ok --> OK["log: synced #lt;hash#gt; #lt;subject#gt;"] I -- rejected --> J{"other files staged?"} J -- yes --> X10["log: failed, no rebase · commit kept locally · exit 1"] J -- no --> K["one pull --rebase --autostash, push again
(never force)"] K -- ok --> OK2["log: synced after rebase"] K -- fails --> X7["abort rebase · commit kept locally · exit 1"]

Each run writes one line to ~/.agent-hub/<name>/logs/sync.log. To turn auto sync off, set HUB_AUTO_SYNC=0 in hub.conf (or run bin/init --auto-sync off on a new hub), then run bin/schedule install again. That removes the sync job, and session end stops pushing.

The secret scan of commits about to be pushed runs only when the branch has an upstream. That's why the first push is done by hand with -u. Paths in the list that don't exist are skipped.

Scheduled jobs

bin/schedule install     # write + load the launchd jobs (run again after pulling new jobs)
bin/schedule status      # loaded / absent for each
bin/schedule uninstall
Job (label)WhenRunsLog
ai.agent-hub.<name>.briefingMon–Fri at HUB_BRIEFING_HOUR:MINUTE, local timebin/briefinglogs/briefing.log
ai.agent-hub.<name>.monitorevery 30 minutesbin/monitorlogs/monitor.log
ai.agent-hub.<name>.syncevery 30 minutes (not installed when HUB_AUTO_SYNC=0)bin/sync --autologs/sync.log

Plists go to ~/Library/LaunchAgents and logs to ~/.agent-hub/<name>/logs/. A job sees only the plist's PATH (~/.local/bin, /opt/homebrew/bin, /usr/local/bin, /usr/bin, /bin), not your shell profile.

The morning briefing

bin/briefing runs the hub's Claude install with -p on the briefing model (Haiku by default). It can only use Read, Grep, Glob, bin/context and bin/brain search. It writes a briefing of 25 lines or fewer covering five things: what's live and healthy, anything broken, what's waiting on you, the client or others, upcoming calls and deadlines, and the top five tasks with their folders. It adds the persona's briefing_focus. The briefing goes to the journal, and a notification says "Morning briefing is in the journal". A failure is journaled as "briefing failed". MONITOR_QUIET=1 silences the briefing's notification as well as the monitors'.

Monitors

A monitor is any executable projects/<x>/monitors.d/*.sh or ops/<x>/monitors.d/*.sh. It must be read-only. The first line of its output is its status, and any line starting with ALERT raises an alert.

#!/bin/bash
# ops/environments/monitors.d/staging.sh
code=$(curl -s -o /dev/null -m 10 -w '%{http_code}' https://staging.example.com/health)
echo "$(date -u '+%a %H:%M UTC') | staging health $code"
[ "$code" = 200 ] || echo "ALERT staging health returned $code"
flowchart LR
  A["bin/monitor
(every 30 min)"] --> B["run each monitor
(timeout MONITOR_TIMEOUT=300 s, first 20 lines)"] B --> C["status = first line,
leading timestamp stripped"] C --> D{"status changed,
or first run?"} D -- yes --> J["journal: monitor #lt;file#gt;: #lt;output#gt;"] B --> E{"ALERT line and
not already alerting?"} E -- yes --> J E -- yes --> N["macOS notification
(MONITOR_QUIET=1 silences)"]

A leading timestamp such as Fri 10:05 UTC | or 2026-10-30 10:05Z | is ignored when comparing statuses, so a new time alone doesn't count as a change. An alert is journaled once and clears when a run has no ALERT line. State is kept in ~/.agent-hub/<name>/monitor-state/.

hub.conf

bin/init writes this file and every script sources it. It holds names and locations only, never secrets. Commit it by hand after each edit, because bin/sync doesn't include it.

Which edits take effect

  • Read on every run: the Claude install and config dir, HUB_MODEL and HUB_EFFORT (for bin/work), the briefing model and focus, the remote box, staging URL, secret names, repos, code owner, tools, brain paths, embedding settings and auto sync. Re-run bin/schedule install after changing the briefing time or auto sync, and bin/setup after changing the Claude install or config dir.
  • Written once, at creation: the client, owner, title and persona model go into CLAUDE.md, ACCESS.md, README.md and the persona agent. HUB_AGENT_MODEL goes into each folder agent file when bin/new makes it. The remote box, staging URL and secret names also go into ACCESS.md. Changing those keys later changes nothing in those files, so edit the files too.
  • Don't rename HUB_NAME on a working hub. It moves the state folder and the launchd labels, and the old jobs keep running (run bin/schedule uninstall first). The Keychain service stays the old name unless you also change HUB_KEYCHAIN_SERVICE.
KeyMeaningEnv override
HUB_NAMEhub name; also the state folder ~/.agent-hub/<name>
HUB_PERSONA, HUB_PERSONA_TITLEpersona slug and title
HUB_CLIENT, HUB_OWNERclient and owner names
HUB_CLAUDEthe Claude install bin/work and bin/briefing run (start the hub-root session with the same command)AGENT_CLAUDE, bin/work --claude
HUB_CLAUDE_CONFIG_DIRits config dir (memory + skills links)shell CLAUDE_CONFIG_DIR when unset
HUB_MODEL, HUB_EFFORTdeep-work sessions (bin/work). The persona agent's model is written once, at init; effort doesn't apply to itbin/work --model / --effort
HUB_AGENT_MODELnew folder agents (bin/new) and bin/work's specialist agentsbin/work --agent-model
HUB_BRIEFING_MODELthe briefingAGENT_BRIEFING_MODEL
HUB_CODE_OWNERGitHub owner for bare repo namesAGENT_CODE_OWNER
HUB_REPOSrepos bin/doctor checks you can read
HUB_REMOTE_HOST, _USER, _PORT, _KEY, _DIRthe SSH box for bin/remoteAGENT_REMOTE_*
HUB_STAGING_URLchecked by bin/doctorAGENT_STAGING_URL
HUB_SECRETSsecret names bin/doctor checks for
HUB_KEYCHAIN_SERVICEKeychain service name (default: the hub name)AGENT_KEYCHAIN_SERVICE
HUB_BRIEFING_HOUR, HUB_BRIEFING_MINUTEbriefing time, local
HUB_BRIEFING_FOCUSextra instruction for the briefing (from the persona)
HUB_TOOLSextra tools bin/doctor checks
HUB_BRAIN_PATHSextra paths for the brain (space-separated)AGENT_BRAIN_PATHS
HUB_EMBED_MODEL, HUB_OLLAMA_URLembedding model and Ollama addressAGENT_EMBED_MODEL, AGENT_OLLAMA_URL
HUB_AUTO_SYNC1 (default) or 0AGENT_AUTO_SYNC (session end only)

Other environment knobs: AGENT_STATE (state folder), AGENT_CODE_DIR (where bin/work clones, default ~/Project), AGENT_TERMINAL=iterm, WORK_DRY=1, MONITOR_TIMEOUT, MONITOR_QUIET=1, AGENT_SYNC_LOG, AGENT_SYNC_LOCK, AGENT_SYNC_CMD, SCHEDULE_DRY=1, DOCTOR_OFFLINE=1. Tests use HUB and CLAUDE_HOME.

Models and Claude installs

Every hub pins one Claude install (HUB_CLAUDE) and its config dir (HUB_CLAUDE_CONFIG_DIR). That way bin/setup links memory and skills into the right place wherever you run it, and bin/doctor tells you when a link is missing.

SettingUsed byOverride for one run
HUB_MODEL, HUB_EFFORTdeep-work sessions; HUB_MODEL also seeds the persona agent's model: at initbin/work --model M --effort E, or edit the agent file
HUB_AGENT_MODELseeds each folder agent's model: when bin/new makes it; default for bin/work's specialist agentsbin/work --agent-model [name=]M, or edit the agent file
HUB_BRIEFING_MODELbin/briefingAGENT_BRIEFING_MODEL=M bin/briefing
HUB_CLAUDEbin/work and bin/briefing; start the hub-root session with this command toobin/work --claude CMD, AGENT_CLAUDE=CMD

Other providers, such as OpenRouter, Ollama or a second account, work through a wrapper command passed as --claude. bin/init reads the wrapper's export CLAUDE_CONFIG_DIR=… line to find its config dir. If you use a wrapper, start the hub-root session with it as well. Plain claude would use another config dir, without the hub's memory and skill links.

How memory is linked

Claude Code keeps auto-memory per working directory under <config dir>/projects/<encoded path>/memory. bin/setup replaces each of these with a symlink into the hub, so memory is committed and travels with the repo:

  • the hub root → memory/
  • each folder → <folder>/memory/
  • each checkout in a folder's Code table → that folder's memory/. A checkout shared by two folders goes to the first one that claims it, and a folder that has the phrase memory owned by anywhere in its ACCESS.md is processed last, so another folder that lists the same checkout claims it first. Later claimants get a warning.

Existing memory there is copied in first, and the old folder is kept as .bak-<time>. BMAD skills are linked into <config dir>/skills/ unless a skill with that name already exists and isn't the hub's.

Secrets and safety

  • Secrets live in the macOS Keychain under the hub's service name. bin/secret --set <name> reads the value from a hidden prompt. bin/secret <name> prints it for $(…) use only. --has, --forget and --list (names only) do what they say.
  • The pre-commit hook (.githooks/pre-commit) refuses anything under repos/, files over 1 MB (unless listed in .bigfiles-allow) and anything gitleaks flags. If gitleaks isn't installed, the hook only warns and the commit goes through, so install it on every Mac.
  • Auto sync also scans every commit about to be pushed, including ones made with --no-verify, when the branch has an upstream. It never force-pushes.
  • The brain never indexes a section that looks like a key, token, password or private key.
  • Gitignored: repos/, .local/, .env*, *.jsonl, PDFs and images, .claude/settings.local.json.
  • bin/remote runs SSH in batch mode with a 10 s connect timeout. Agents may run read-only commands. Anything that writes needs your word for that change.
  • bin/doctor never prints a secret and always exits 0.

Agent rules

The rendered CLAUDE.md binds every agent in the hub. In short:

  • Verified, not vibed. Work test-first in the client repo. Tests and a live check decide Done, and agents report real output, failures included.
  • Every answer cites a source opened in this session: path:line, a URL, a command's output or a hub file. Without one the answer is "unverified".
  • Code changes go in the client's repos, on a branch, with small commits and tests. They follow each repo's own rules.
  • Commits and PRs carry only the owner's git identity, with no AI attribution.
  • Deploys, writes to shared systems and anything production need the owner's word for that change. Each deploy is verified live and journaled.
  • Client decisions belong to the client and are recorded with who and when. Messages to the client are drafts only.
  • Proposals include a Revenue / cost impact section with numbers, sources and labelled assumptions (templates/proposal.md).
  • Token budget: read on demand, send long output to a file, reuse STATE and the journal, keep replies short.
  • BMAD v6 skills (bmad-prd, bmad-spec, bmad-ticket, bmad-build, bmad-code-review, …) are used for planning and builds. Their output stays in the hub's _bmad-output/, never in the client repo. The ADE rules in .ade/rules.md win over BMAD.

Command reference

CommandDoes
bin/init … / --listturn the template into a hub; list personas
bin/setuplink memory and skills into the Claude config dir; set the git hooks path
bin/doctorOK / MISSING for tools, the Claude install, memory link, brain index, SSH key, GitHub login, repos, remote box, staging, embeddings, secrets
bin/context [folder] [--rules]every folder's state, or one folder's state and journal
bin/work [opts] <folder> [repo]deep work in a new Terminal in the folder's checkout
bin/new projects/<name> · ops/<name>a new folder and its agent; never overwrites
bin/journal <folder> "<text>"one dated entry (hub for the root)
bin/brain index [--full] · search [-n N] [--json] <q> · statusthe searchable memory
bin/remote <cmd>a command on the remote box over SSH, in HUB_REMOTE_DIR
bin/secret <name> · --set · --has · --forget · --listKeychain secrets
bin/sync · --yes · --autoreview, commit and push hub files
bin/schedule install|uninstall|statusthe launchd jobs
bin/briefingthe morning briefing, now
bin/monitorrun every monitor now
bin/changes, bin/stop-check, bin/session-endhooks (run by Claude Code, not by hand)

A new Mac or a fresh clone

STATE.md, memory, the journal and ACCESS.md are committed, so a fresh clone starts with full context:

gh repo clone <you>/my-acme-sre && cd my-acme-sre
bin/setup && bin/brain index
bin/doctor                     # shows what this Mac is missing
bin/secret --set <name>        # for each secret doctor lists
bin/schedule install

Secrets, SSH keys and files outside the repo (downloads, PDFs) don't travel. bin/doctor lists what's missing. This works only if hub.conf was committed (see Make a hub).

Clone to the same absolute path. Several committed files hold the first Mac's absolute paths: each folder's .claude/settings.json (the hook commands), HUB_CLAUDE_CONFIG_DIR in hub.conf, and output_folder in _bmad/config.toml. Use the same user name and folder. Otherwise, fix those values: move each folder's .claude/settings.json aside and run bin/new <folder> again, edit hub.conf and _bmad/config.toml, then run bin/setup and bin/schedule install.

Upgrading a hub made before auto sync

  1. Folder sessions only hear about hub changes through the UserPromptSubmit hook, and bin/new keeps an existing folder settings file. For each folder, move <folder>/.claude/settings.json aside and run bin/new <folder> again, or add this line under "hooks":
    "UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "\"/path/to/hub\"/bin/changes projects/<x>" }] }],
  2. Run bin/schedule install again to load the monitor and sync jobs.
  3. Auto sync is on unless hub.conf says HUB_AUTO_SYNC=0. It needs gitleaks and bin/setup; without them every run is refused and logged.

Troubleshooting

You seeDo
MISSING hub memory linkbin/setup (check HUB_CLAUDE_CONFIG_DIR points at the install you use)
MISSING brain index … changed since indexingbin/brain index
MISSING embeddingsollama pull nomic-embed-text; search stays keyword-only until then
MISSING remote sshload the key (ssh-add) or check the host; never copy the key into the hub
MISSING secret <name>bin/secret --set <name>
sync.log: refused: gitleaks not installedbrew install gitleaks
sync.log: refused: core.hooksPath is not .githooksbin/setup
sync.log: refused: gitleaks found a secretremove it from the unpushed commits; nothing was pushed
sync.log: failed: push rejected and other files are stagedcommit or unstage those files, then bin/sync --yes
sync.log: failed: commit refused (pre-commit hook, …)run bin/sync --yes to see the hook's reason (repos/, over 1 MB, or a secret)
sync.log: skipped: git is busy (index.lock) on every runa crashed git left .git/index.lock; delete it when no git command is running
sync.log says synced, but nothing reaches GitHubno origin: git remote add origin <url>, then git push -u origin main
Deep-work hooks fail with "no such file"the hub moved; see A new Mac
REFUSED: … is over 1 MBkeep it out, or list it in .bigfiles-allow
A monitor works by hand but not on schedulelaunchd jobs only see the plist's PATH; use full paths, and keep it under 300 s
Briefing failed in the journalrun bin/briefing by hand; check logs/briefing.log and that HUB_CLAUDE is logged in
work: … has no ownerwrite owner/name in ACCESS.md or set HUB_CODE_OWNER

Working on the template

  • Keep it client-neutral: no client, person, host or account names outside tests/ fixtures (tests/test_root.sh checks).
  • Hub rules live in templates/hub/CLAUDE.md, personas in personas/<slug>.md, and folder files in templates/.
  • The bin/ scripts are covered by tests/test_*.sh, which run against a throwaway copy with stubbed tools. The verification command is:
bash tests/run.sh       # every tests/test_*.sh prints ok; exit 0 = green