Makes an apiaryOs-enabled (HIVE) session unmistakable, and keeps apiaryOs tools out of a plain session. State is fixed at launch from APIARY_CAPTURE_DISABLED.

A Claude Code mod plus a launcher. Two kinds of session, decided at launch, no in-session toggle:
hive | plain claude | |
|---|---|---|
| apiaryOs MCP server | loaded from apiaryos.mcp.json | not loaded |
| transcript capture (apiary-capture) | ON (APIARY_CAPTURE_DISABLED=0) | OFF (mod pins =1) |
| apiaryOs tool calls | allowed | refused by a tool.call hook |
| system prompt | apiaryOs default-tool + "I drive, you execute" | "apiaryOs not here, use local skills, never improvise" |
| look | amber band, 🐝 HIVE footer, 🐝 on your prompts, chime, pane tint | nothing |
/hive in either session says which one you are in.
apiary-capture uploads a session as soon as any apiaryOs tool_use lands in the transcript, and a denied call still leaves that block there. So the deny is a belt; APIARY_CAPTURE_DISABLED is the guarantee. bin/hive is the only thing that sets it to 0; the mod pins it to 1 otherwise.
Prereqs: Claude Code ≥ 2.1.296, the apiary-capture@implentio plugin (/plugin, marketplace URL in the stack repo's tools/claude-plugins/apiary-capture/README.md).
~/.claude/mods/hive. In these dotfiles, ./install links it (dotbot). Anywhere else: git clone/copy, then ln -s <folder> ~/.claude/mods/hive.sh claude() { APIARY_CAPTURE_DISABLED=1 command claude --plugin-dir ~/.claude/mods/hive "$@"; } export PATH="$HOME/.claude/mods/hive/bin:$PATH" ``hive: a browser opens for the Authentik login (once per machine; the OAuth token is shared across Claude Code scopes). Then run /apiary-capture:setup if capture isn't set up yet.claude mcp add apiaryos in user/project scope and do NOT add the apiaryOs block to ~/.claude/CLAUDE.md. Both would leak apiaryOs into plain sessions.| Piece | Works on | If unsupported |
|---|---|---|
| mod (band, footer, prompt, deny, chime) | anywhere Claude Code runs | n/a |
| capture token | macOS Keychain, Linux Secret Service (secret-tool), Windows DPAPI file | see apiary-capture README |
| pane tint | tmux (any OS) | skipped |
| terminal background tint | Ghostty, kitty, WezTerm, foot, iTerm2 | ignored silently (Terminal.app, Windows Terminal) |
cd ~/.claude/mods/hive
claude plugin validate . && claude plugin test .
Load the plugin-authoring skill in a session first; it writes the current build's type definitions to .claude-plugin/types/ (git-ignored). Roadmap in PLAN.md.
Marketplace entry in the stack repo (tools/claude-plugins/hive) and a nix devshell hive command would remove steps 1–2. Deliberately not done yet; see PLAN.md § open questions.
hooks/register.tsx 100 lines1import type { Register } from 'claude-code'
2
3// The capture plugin's own kill switch is the single source of truth: a session is HIVE exactly
4// when capture.sh would upload it. The launcher sets it; nothing in-session can change it.
5const APIARY_TOOL = /^mcp__[A-Za-z0-9_]*apiary[A-Za-z0-9_]*__/i
6const AMBER = '#f5b700'
7const DARK = '#2a1f00'
8
9// The standing instruction apiaryOs onboarding would put in ~/.claude/CLAUDE.md, applied to HIVE
10// sessions only, so plain sessions never try a tool they do not have.
11const HIVE_PROMPT = `This is a HIVE session: apiaryOs is live and this transcript is captured to apiaryOs conversation memory.
12
13## apiaryOs (Implentio)
14apiaryOs is Implentio's default tool gateway and the primary tool for all Implentio work (brands, invoices, billers, reconciliation, onboarding, Implentio systems and data).
15- In a conversation about Implentio work, call the apiaryOs \`session_start\` tool first and follow its next_action. Subagents do not need to call it.
16- When the person uses an Implentio term you are not certain of, look them all up in one \`term_lookup\` call before investigating or reading code.
17- Every time a distinct problem comes into focus, and again whenever it changes, call \`conversation_search\` with it before investigating. Start from where the closest past resolution ended, re-verify it against the current state, and say what you found ("I found other Implentio conversations around this: ...").
18- Prefer apiaryOs tools over any other way of reading or changing Implentio systems. Fall back to local skills (implentio-db, trino, argo, kubectl) only for what apiaryOs cannot do yet.
19
20### I drive, you execute
21In Implentio work, the person owns the framing, the criteria and the decision; you execute and challenge their reasoning.
22- Routine work (drafting from notes, edits, summaries, formatting, code to a clear spec, lookups): just do it.
23- If asked to decide with no view given, first ask one short question for their position, criteria and constraints. If they have none, lay out the options and trade-offs and leave the choice to them.
24- When they share a view, test it: weak assumptions, missing facts, the strongest counterargument. Do not just agree.
25- On high-stakes calls (credit memos, rates, customer commitments, legal, personnel, strategy), say the decision is theirs and what they should verify.
26- Flag facts or figures that may be wrong or stale, and how to check them.
27- Never lecture about AI use. Subagents skip this section.`
28
29export const register: Register = on => {
30 let isHive = false
31 on('session.start', async ($, e, next) => {
32 // Only an explicit 0 (set by bin/hive) makes a HIVE session. Unset means plain, and the mod
33 // pins the kill switch so capture.sh sees it too, without any shell-side wrapper for `claude`.
34 isHive = (await $.env.get('APIARY_CAPTURE_DISABLED')) === '0'
35 if (!isHive) await $.env.set('APIARY_CAPTURE_DISABLED', '1')
36 await $.command.register({
37 name: 'hive',
38 description: 'Say whether this session is HIVE (apiaryOs live, transcript uploads) or plain',
39 })
40 if (isHive) {
41 $.ui.status('🐝 HIVE')
42 $.ui.toast('🐝 HIVE session: apiaryOs is live and every turn uploads to conversation memory', { timeoutMs: 8000 })
43 void $.audio.play({ asset: 'fx/hive.wav' }).catch(() => undefined)
44 }
45 return next(e)
46 })
47
48 on('command.run', { command: 'hive' }, () => ({
49 text: isHive
50 ? '🐝 HIVE session. apiaryOs tools are live and this transcript uploads to apiaryOs conversation memory. Fixed at launch; start a plain `claude` to leave.'
51 : 'Plain session. apiaryOs tools are refused and nothing uploads. Fixed at launch; start `hive` to use apiaryOs.',
52 }))
53
54 // Belt for a plain session that still has the server configured somewhere.
55 on('tool.call', ($, e, next) =>
56 !isHive && APIARY_TOOL.test(e.tool)
57 ? { deny: 'hive: apiaryOs is off in this plain session. Use local skills (implentio-db, trino, argo), or ask the person to relaunch with `hive`.' }
58 : next(e),
59 ).catch(($, e, next) => (next.called ? next(e) : { deny: 'hive: its guard failed; apiaryOs refused.' }))
60
61 on('prompt.compose', async ($, e, next) => {
62 const r = await next(e)
63 const text = isHive
64 ? HIVE_PROMPT
65 : 'This is a plain session: apiaryOs is not loaded and every apiaryOs MCP tool is refused here. Do not call them; use local skills instead. If the person asks for an apiaryOs tool (session_start, conversation_search, brand_list, ...), answer in one line that apiaryOs is not in this session and that a `hive` session has it. Never improvise a stand-in answer. apiaryOs cannot be enabled mid-session.'
66 return { sections: [...r.sections, { id: 'hive:mode', text, scope: 'session' }] }
67 })
68
69 on('ui.render', { component: 'AbovePrompt' }, ($, e, next) => {
70 if (!isHive || e.props.hasSurvey) return next(e)
71 const { Box, Text } = $.ui.resolve(e)
72 const w = e.props.bodyColumns ?? 80
73 const label = ' 🐝 HIVE · apiaryOs LIVE · this conversation uploads to apiaryOs memory '
74 const pad = Math.max(0, w - label.length - 1)
75 return (
76 <Box flexDirection="column">
77 <Box backgroundColor={AMBER} width={w}>
78 <Text color={DARK} bold>{label}{' '.repeat(pad)}</Text>
79 </Box>
80 <Box backgroundColor={DARK} width={w}>
81 <Text color={AMBER}>{'▰'.repeat(Math.max(1, w - 1))}</Text>
82 </Box>
83 </Box>
84 )
85 })
86
87 on('ui.render', { component: 'SessionMode' }, ($, e, next) =>
88 isHive ? next({ ...e, props: { ...e.props, modes: ['🐝 HIVE', ...e.props.modes] } }) : next(e),
89 )
90
91 on('ui.render', { component: 'Spinner' }, ($, e, next) =>
92 isHive ? next({ ...e, props: { ...e.props, word: 'Buzzing' } }) : next(e),
93 )
94
95 // Every prompt row carries the mark, so scrollback says which kind of session it was.
96 on('ui.render', { component: 'UserMessage' }, ($, e, next) =>
97 isHive && e.props.origin.kind === 'composer' ? next({ ...e, props: { ...e.props, text: `🐝 ${e.props.text}` } }) : next(e),
98 )
99}
100