macOS menubar daemon + CLI for displaying system toast notifications. Designed as a notification bridge for Claude Code hooks and other AI agent workflows.
# Simple notification
codo "Build Done" "All 42 tests passed"
# With template (sets subtitle + sound automatically)
codo "Build Done" "42 tests passed" --template success
codo "Build Failed" "3 errors" --template error
codo "Deploying..." --template progress
# Group related notifications
codo "Step 1/3" --template progress --thread deploy-v1.2
codo "Step 2/3" --template progress --thread deploy-v1.2
codo "Deploy Done" --template success --thread deploy-v1.2
# Via stdin JSON
echo '{"title":"Build Done","body":"All tests passed","subtitle":"β
Success"}' | codo
# List available templates
codo --template listTwo layers: Swift menubar app (daemon, listens on Unix Domain Socket, shows toast) + TypeScript CLI (Bun script, sends messages to daemon).
Claude Code hook β codo CLI (Bun) β UDS β Codo.app (Swift) β macOS notification
- macOS 14+ (Sonoma)
- Swift 5.10+ (
swift --version) - Bun (
bun --version) β CLI runtime and TS test runner - SwiftLint (
brew install swiftlint)
# Install dependencies and git hooks
bun installThis runs husky via the prepare script, which sets up .husky/ as the git hooks directory.
| Hook | Stage | What runs |
|---|---|---|
| pre-commit | L1+L2 | swift test + bun test + SwiftLint strict + Biome lint |
| pre-push | L1+L2+L3 | Unit tests + lint + integration tests |
Hooks cannot be skipped β this is by design. Every commit must pass unit tests and lint.
# L1: Swift unit tests (46 tests)
swift test
# L1: TypeScript unit tests (60 tests)
cd cli && bun test
# L2: Swift lint
swiftlint lint --strict --quiet
# L2: TypeScript lint
cd cli && bunx biome check .
# L3: Integration tests (16 tests)
./scripts/integration-test.sh
# L4: E2E manual test
./scripts/e2e-test.sh
# Swift coverage report (target: 90%+ on CodoCore)
swift test --enable-code-coverage
xcrun llvm-cov report \
.build/debug/CodoPackageTests.xctest/Contents/MacOS/CodoPackageTests \
--instr-profile=.build/debug/codecov/default.profdata \
--sources Sources/CodoCore/| Target | Current | Goal |
|---|---|---|
| CodoCore (testable) | 91% | β₯ 90% |
| CLI (parseArgs/parseStdin) | 100% | β |
SystemNotificationProvider is excluded from coverage β it requires a real .app bundle with UNUserNotificationCenter and is tested via L4 E2E manual checklist.
# Build signed .app bundle
./scripts/build.sh
# Install to ~/Applications + symlink CLI to /usr/local/bin
./scripts/install.shcodo <title> [body] [--template <name>] [--subtitle <text>] [--thread <id>] [--silent]| Flag | Description |
|---|---|
--template <name> |
Apply a notification template (sets subtitle + sound) |
--subtitle <text> |
Set notification subtitle (overrides template) |
--thread <id> |
Group notifications by thread ID |
--silent |
Suppress notification sound (overrides template) |
--template list |
List all available templates |
| Template | Subtitle | Sound | Use Case |
|---|---|---|---|
success |
β Success | default | Build passed, tests green |
error |
β Error | default | Build failed, test failures |
warning |
default | Lint warnings, deprecations | |
info |
βΉοΈ Info | none | Status updates (silent) |
progress |
π In Progress | none | Long-running tasks (silent) |
question |
β Action Needed | default | User input required |
deploy |
π Deploy | default | Deployment lifecycle |
review |
π Review | default | PR/code review requests |
Codo includes a hook script that receives all Claude Code events and routes them to the daemon. After installing, configure ~/.claude/settings.json to point at the script:
{
"hooks": {
"Stop": [
{ "hooks": [{ "type": "command", "command": "~/.codo/hooks/claude-hook.sh" }] }
],
"SubagentStop": [
{ "hooks": [{ "type": "command", "command": "~/.codo/hooks/claude-hook.sh" }] }
],
"Notification": [
{ "hooks": [{ "type": "command", "command": "~/.codo/hooks/claude-hook.sh" }] }
],
"PostToolUse": [
{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "~/.codo/hooks/claude-hook.sh" }] }
],
"PostToolUseFailure": [
{ "hooks": [{ "type": "command", "command": "~/.codo/hooks/claude-hook.sh" }] }
],
"SessionStart": [
{ "hooks": [{ "type": "command", "command": "~/.codo/hooks/claude-hook.sh" }] }
],
"SessionEnd": [
{ "hooks": [{ "type": "command", "command": "~/.codo/hooks/claude-hook.sh" }] }
]
}
}Debug with CODO_DEBUG_HOOKS=1 β output goes to stderr only, never blocks the agent. See docs/features/04-claude-hook-integration.md for the full design.
See docs/ for design documents:
- Architecture β System design, IPC protocol, build, testing, MVP plan
- Features β Feature iteration docs (notification templates)