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.
| Tool | Used for | Needed? |
|---|---|---|
claude | Claude Code: every session, the briefing | Yes |
git, python3 | the hub itself; bin/init, bin/work, bin/changes, bin/brain | Yes |
gh | creating the hub from the template, cloning client repos, checking repo access | Yes, for GitHub use |
gitleaks | the 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 run | Yes |
ssh, curl, node | bin/remote, the staging check, and tools the agents may need. bin/doctor checks for all three | Checked |
Ollama + nomic-embed-text | meaning-based search in bin/brain | Optional (keyword search works without it) |
timeout / gtimeout | monitor and index time limits | Optional (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.
- 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-sreWithout GitHub: run
bin/init … --dest ~/Project/acme-srefrom 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 logssynced. - 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,--clientand--ownerare required.bin/initnever overwrites a hub that already has ahub.conf, and it writes no secrets. - Commit and push the hub once, by hand.
git add -A && git commit -m "Initialize the hub" && git push -u origin mainbin/initcommits nothing, andbin/sync's path list leaves outhub.conf. Without this commit, a fresh clone has nohub.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-ualso sets the upstream that auto sync needs before it can scan commits ahead of a push. Commithub.confby hand again whenever you edit it. - 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 - Turn on the scheduled jobs.
bin/schedule install # weekday briefing, monitor + sync every 30 min - 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
| Option | Sets | Default |
|---|---|---|
--persona <slug> | the role (a file in personas/); --list shows them | required |
--client, --owner | who the work is for, and who the agent works for | required |
--name | the 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 first | initialize 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 happens | none |
--code-owner <org> | the GitHub owner for repos named without one | none |
--remote user@host[:port] | the box bin/remote reaches over SSH | none (port 22) |
--remote-key, --remote-dir | the SSH key path, and the directory commands run in | none |
--staging-url | a 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:MM | the weekday morning-briefing time, this Mac's local time | 08:00 |
--claude <cmd> | the Claude install to run: claude, or a wrapper that sets its own CLAUDE_CONFIG_DIR | claude |
--claude-config-dir <dir> | that install's config dir, where memory and skills are linked | read from the wrapper's export CLAUDE_CONFIG_DIR=, else ~/.claude |
--model M, --effort E | deep-work sessions; the model also seeds the persona agent at init; effort is low|medium|high|xhigh|max and applies to bin/work only | the persona's model |
--agent-model M | the folder agents' model (written when each is made) and bin/work's specialist agents | sonnet |
--briefing-model M | the morning briefing | haiku |
--brain-path <path> | extra folders, files or globs the brain indexes (repeatable; quote globs) | none |
--embed-model M, --ollama-url URL | the local embedding model and Ollama address | nomic-embed-text, http://localhost:11434 |
--auto-sync on|off | HUB_AUTO_SYNC: push every 30 minutes and at each folder session's end | on |
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
| Slug | Role | Default folders | Model |
|---|---|---|---|
data-engineer | Data Engineer | projects/pipelines, projects/warehouse, projects/reporting, ops/data-quality | sonnet |
devops-sre | DevOps / Site Reliability Engineer | ops/environments, ops/ci-cd, ops/monitoring, ops/security | sonnet |
engineering-manager | Engineering Manager | projects/team, projects/delivery, projects/client | sonnet |
fractional-cto | Fractional CTO | projects/strategy, projects/proposals, projects/vendors, projects/client | opus |
frontend-engineer | Frontend Engineer | projects/web, projects/design-system, projects/client | sonnet |
ml-ai-engineer | ML / AI Engineer | projects/models, projects/agents, projects/evals, projects/proposals | opus |
mobile-engineer | Mobile Engineer | projects/ios, projects/android, projects/releases | sonnet |
qa-test-engineer | QA / Test Engineer | projects/app, projects/test-plans, ops/environments | sonnet |
security-engineer | Security Engineer | ops/security, ops/access, projects/app, projects/reviews | opus |
solutions-architect | Solutions Architect | projects/architecture, projects/proposals, projects/client | opus |
staff-software-engineer | Staff Software Engineer | projects/app, projects/proposals, projects/client, ops/environments | opus |
tech-lead | Tech Lead | projects/app, projects/planning, projects/client | sonnet |
technical-product-manager | Technical Product Manager | projects/roadmap, projects/specs, projects/client | sonnet |
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
| File | Holds |
|---|---|
CLAUDE.md | Two 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.md | Code 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.md | Updated: 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
UserPromptSubmithook 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
| Event | Hub root | Folder session |
|---|---|---|
| SessionStart | bin/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 entries | bin/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 |
| UserPromptSubmit | bin/changes | bin/changes <folder>; also shows hub-root journal entries. Its own folder isn't echoed back |
| PreCompact | journals a reminder to update the folders' STATE.md | journals that STATE.md and the entry should say what was in progress |
| Stop | none | bin/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) |
| SessionEnd | none | bin/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-remoteturns 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 mattername,description, optionaltoolsandmodel) are passed to the session as--agents.bin/newdoesn't create any; add them yourself.HUB_AGENT_MODELis their default model.--agent-model sonnetor--agent-effort highsets all of them, and--agent-model reviewer=opussets 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 forbin/work. - A repo row marked
checkout owned by <folder>with an empty Checkout sends you to that folder instead. WORK_DRY=1prints the command without opening a window.AGENT_TERMINAL=itermuses 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,targetand every hidden folder. The same list applies insideHUB_BRAIN_PATHSfolders, so a clienttest/orbuild/folder there is skipped too. - A memory file's
nameanddescriptiontitle its first section. - A folder session's end re-indexes (the hub root has no SessionEnd hook), and
bin/doctorreports 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.
| Mode | Does |
|---|---|
bin/sync | Review only: lists pending changes and a diff stat, and notes untracked files under .claude/ that --yes would include. |
bin/sync --yes | Commits sync <date> <time> (through the pre-commit hook) and pushes. |
bin/sync --auto | The 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) | When | Runs | Log |
|---|---|---|---|
ai.agent-hub.<name>.briefing | Mon–Fri at HUB_BRIEFING_HOUR:MINUTE, local time | bin/briefing | logs/briefing.log |
ai.agent-hub.<name>.monitor | every 30 minutes | bin/monitor | logs/monitor.log |
ai.agent-hub.<name>.sync | every 30 minutes (not installed when HUB_AUTO_SYNC=0) | bin/sync --auto | logs/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_MODELandHUB_EFFORT(forbin/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-runbin/schedule installafter changing the briefing time or auto sync, andbin/setupafter 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.mdand the persona agent.HUB_AGENT_MODELgoes into each folder agent file whenbin/newmakes it. The remote box, staging URL and secret names also go intoACCESS.md. Changing those keys later changes nothing in those files, so edit the files too. - Don't rename
HUB_NAMEon a working hub. It moves the state folder and the launchd labels, and the old jobs keep running (runbin/schedule uninstallfirst). The Keychain service stays the old name unless you also changeHUB_KEYCHAIN_SERVICE.
| Key | Meaning | Env override |
|---|---|---|
HUB_NAME | hub name; also the state folder ~/.agent-hub/<name> | |
HUB_PERSONA, HUB_PERSONA_TITLE | persona slug and title | |
HUB_CLIENT, HUB_OWNER | client and owner names | |
HUB_CLAUDE | the 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_DIR | its config dir (memory + skills links) | shell CLAUDE_CONFIG_DIR when unset |
HUB_MODEL, HUB_EFFORT | deep-work sessions (bin/work). The persona agent's model is written once, at init; effort doesn't apply to it | bin/work --model / --effort |
HUB_AGENT_MODEL | new folder agents (bin/new) and bin/work's specialist agents | bin/work --agent-model |
HUB_BRIEFING_MODEL | the briefing | AGENT_BRIEFING_MODEL |
HUB_CODE_OWNER | GitHub owner for bare repo names | AGENT_CODE_OWNER |
HUB_REPOS | repos bin/doctor checks you can read | |
HUB_REMOTE_HOST, _USER, _PORT, _KEY, _DIR | the SSH box for bin/remote | AGENT_REMOTE_* |
HUB_STAGING_URL | checked by bin/doctor | AGENT_STAGING_URL |
HUB_SECRETS | secret names bin/doctor checks for | |
HUB_KEYCHAIN_SERVICE | Keychain service name (default: the hub name) | AGENT_KEYCHAIN_SERVICE |
HUB_BRIEFING_HOUR, HUB_BRIEFING_MINUTE | briefing time, local | |
HUB_BRIEFING_FOCUS | extra instruction for the briefing (from the persona) | |
HUB_TOOLS | extra tools bin/doctor checks | |
HUB_BRAIN_PATHS | extra paths for the brain (space-separated) | AGENT_BRAIN_PATHS |
HUB_EMBED_MODEL, HUB_OLLAMA_URL | embedding model and Ollama address | AGENT_EMBED_MODEL, AGENT_OLLAMA_URL |
HUB_AUTO_SYNC | 1 (default) or 0 | AGENT_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.
| Setting | Used by | Override for one run |
|---|---|---|
HUB_MODEL, HUB_EFFORT | deep-work sessions; HUB_MODEL also seeds the persona agent's model: at init | bin/work --model M --effort E, or edit the agent file |
HUB_AGENT_MODEL | seeds each folder agent's model: when bin/new makes it; default for bin/work's specialist agents | bin/work --agent-model [name=]M, or edit the agent file |
HUB_BRIEFING_MODEL | bin/briefing | AGENT_BRIEFING_MODEL=M bin/briefing |
HUB_CLAUDE | bin/work and bin/briefing; start the hub-root session with this command too | bin/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 phrasememory owned byanywhere in itsACCESS.mdis 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,--forgetand--list(names only) do what they say. - The pre-commit hook (
.githooks/pre-commit) refuses anything underrepos/, 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/remoteruns 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/doctornever 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.mdwin over BMAD.
Command reference
| Command | Does |
|---|---|
bin/init … / --list | turn the template into a hub; list personas |
bin/setup | link memory and skills into the Claude config dir; set the git hooks path |
bin/doctor | OK / 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> · status | the searchable memory |
bin/remote <cmd> | a command on the remote box over SSH, in HUB_REMOTE_DIR |
bin/secret <name> · --set · --has · --forget · --list | Keychain secrets |
bin/sync · --yes · --auto | review, commit and push hub files |
bin/schedule install|uninstall|status | the launchd jobs |
bin/briefing | the morning briefing, now |
bin/monitor | run every monitor now |
bin/changes, bin/stop-check, bin/session-end | hooks (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).
.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
- Folder sessions only hear about hub changes through the
UserPromptSubmithook, andbin/newkeeps an existing folder settings file. For each folder, move<folder>/.claude/settings.jsonaside and runbin/new <folder>again, or add this line under"hooks":"UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "\"/path/to/hub\"/bin/changes projects/<x>" }] }], - Run
bin/schedule installagain to load the monitor and sync jobs. - Auto sync is on unless
hub.confsaysHUB_AUTO_SYNC=0. It needs gitleaks andbin/setup; without them every run is refused and logged.
Troubleshooting
| You see | Do |
|---|---|
MISSING hub memory link | bin/setup (check HUB_CLAUDE_CONFIG_DIR points at the install you use) |
MISSING brain index … changed since indexing | bin/brain index |
MISSING embeddings | ollama pull nomic-embed-text; search stays keyword-only until then |
MISSING remote ssh | load 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 installed | brew install gitleaks |
sync.log: refused: core.hooksPath is not .githooks | bin/setup |
sync.log: refused: gitleaks found a secret | remove it from the unpushed commits; nothing was pushed |
sync.log: failed: push rejected and other files are staged | commit 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 run | a crashed git left .git/index.lock; delete it when no git command is running |
sync.log says synced, but nothing reaches GitHub | no 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 MB | keep it out, or list it in .bigfiles-allow |
| A monitor works by hand but not on schedule | launchd jobs only see the plist's PATH; use full paths, and keep it under 300 s |
| Briefing failed in the journal | run bin/briefing by hand; check logs/briefing.log and that HUB_CLAUDE is logged in |
work: … has no owner | write 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.shchecks). - Hub rules live in
templates/hub/CLAUDE.md, personas inpersonas/<slug>.md, and folder files intemplates/. - The
bin/scripts are covered bytests/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