Mori integrates with coding agents (Claude Code, Codex CLI, Pi, and Droid) to display their status in tab names and send notifications. Hooks are manually enabled/disabled in Settings > Agent Hooks. Once enabled, Mori refreshes the installed hook files on every launch so ~/.config/mori/ stays aligned with the current app bundle.
When enabled, each agent installs hook scripts that report state changes to tmux via pane options:
@mori-agent-state:"working"(agent is running),"waiting"(agent finished turn, awaiting input), or"done"(agent exited)@mori-agent-name: Agent name (e.g.,"claude","codex","pi")
During Mori's 5-second polling cycle, it reads these options directly from tmux — no process scanning, no pattern matching. Tab names show the agent name (e.g., claude, codex, pi), with sidebar badges (⚡ working / ❗ waiting / ✅ done). macOS notifications fire on state transitions.
- Open Mori Settings > Agent Hooks
- Toggle Claude Code on
- Mori updates
~/.claude/settings.jsonwith hooks for:UserPromptSubmit(prompt entered)Stop(agent stopped)Notification(completion notification)
- On future launches, Mori re-copies the hook script from its bundled resources if the installed file is stale or missing.
During installation and refresh, Claude Code and Droid remove only their obsolete Mori PreToolUse command left by older releases, preserving other hooks in the same configuration.
Run Claude Code in a tmux window. Tab renames to claude while working (⚡ badge shown in sidebar), then shows waiting badge (❗) when the turn completes. Click the waiting badge to reply inline.
- Open Mori Settings > Agent Hooks
- Toggle Codex CLI on
- Mori creates/updates
~/.codex/hooks.jsonwith only these lifecycle hooks:{ "hooks": { "UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "/Users/you/.config/mori/hooks/mori-codex-hook.sh UserPromptSubmit" }] }], "Stop": [{ "hooks": [{ "type": "command", "command": "/Users/you/.config/mori/hooks/mori-codex-hook.sh Stop" }] }] } }
Mori leaves unrelated Codex hooks intact. On refresh, an existing legacy Mori notify registration in ~/.codex/config.toml is migrated to this format and removed without changing other notify entries.
On future launches, Mori re-copies the hook script from its bundled resources if the installed file is stale or missing.
Run Codex in a tmux window. Tab renames to codex while working (⚡ badge shown in sidebar), then shows waiting badge (❗) when the turn completes.
- Open Mori Settings > Agent Hooks
- Toggle Pi on
- Mori creates a TypeScript extension at
~/.config/mori/mori-pi-extension.tsand registers it in~/.pi/agent/settings.json:{ "extensions": [ "~/mori-pi-extension.ts" ] } - On future launches, Mori re-copies the extension from its bundled resources if the installed file is stale or missing.
Run Pi in a tmux window. Tab renames to pi while working (⚡ badge shown in sidebar), then shows waiting badge (❗) when the turn completes. Click the waiting badge to reply inline.
Hook scripts are stored in $XDG_CONFIG_HOME/mori/hooks/ (fallback: ~/.config/mori/hooks/) and are loaded from Mori's bundle resources. They're shared shell/TypeScript utilities:
Shared bash functions sourced by agent-specific scripts. Sets @mori-agent-state and @mori-agent-name pane options, then renames the tmux window to the agent name.
Responds to Claude Code hook events:
UserPromptSubmit→ state:"working"Stop,Notification→ state:"waiting"
Drains stdin (Claude Code pipes JSON hook data) and bails silently if not in tmux.
Handles the two registered Codex events:
UserPromptSubmit→ state:"working"Stop→ state:"waiting"
The script also accepts legacy notify JSON ({"type":"agent-turn-complete",...}) as a completion event so already-running Codex processes remain compatible during migration. Empty, malformed, and unrecognized arguments do not change state.
TypeScript extension listening to Pi events:
agent_start→ state:"working"agent_settled→ state:"waiting"after all automatic retries, compaction retries, and queued follow-ups finish
Toggle the agent off in Settings > Agent Hooks. Mori removes:
- Hook script from
~/.config/mori/hooks/ - Hook entries from agent config files (
~/.claude/settings.json,~/.codex/hooks.json, and legacy Mori entries in~/.codex/config.toml, etc.)
If you manually delete hook scripts, toggle off then back on to reinstall.
When a hook-detected agent exits (tmux pane returns to shell), Mori automatically:
- Clears
@mori-agent-stateand@mori-agent-namepane options - Re-enables
automatic-renameso tmux updates the window name to the current process
This prevents stale agent names from lingering after the agent has stopped.
When an agent completes (state: "done"), Mori sends a macOS notification:
- Title:
"Claude Code Finished"(or"Codex Finished", etc.) - Body: Window title and worktree name
- Sound: Default system sound
Click the notification to focus the window. Bundled .app builds use UNUserNotificationCenter; unbundled swift run builds fall back to osascript.
Hooks don't seem to fire:
- Verify the hook script exists:
ls ~/.config/mori/hooks/ - Check the agent config file was updated correctly:
- Claude Code:
cat ~/.claude/settings.json | grep mori - Codex:
cat ~/.codex/hooks.json | grep mori - Pi:
cat ~/.pi/agent/settings.json | grep mori
- Claude Code:
- Ensure you're running the agent inside a tmux session. Hooks only work in tmux.
Tab name doesn't change to agent name:
- Reload Mori's tmux polling by switching to another window and back, or restart Mori.
- Check
tmux list-panes -F "#{pane_id} #{@mori-agent-state} #{@mori-agent-name}"to see if pane options are set. - Badge should appear in the sidebar even if tab name hasn't updated yet (verify badge shows ⚡ or ✅).
Hook script permission denied:
- Reinstall via Settings > Agent Hooks toggle (off then on). Mori sets
0755permissions on install.
Codex hooks don't get added:
- Verify
~/.codex/hooks.jsonis readable JSON. - If you added Mori hooks after Codex created the file, toggle the hook off/on to retry.
- Mori preserves unrecognized or malformed hook files rather than overwriting them; repair invalid JSON first.
AgentHookConfigurator (Swift):
- Detects installed hooks:
isClaudeHookInstalled(),isCodexHookInstalled(),isPiExtensionInstalled() - Installs/uninstalls:
installClaudeHook(),uninstallClaudeHook(), etc. - Handles agent config file mutations (JSON for Claude/Codex/Pi; legacy TOML cleanup for Codex)
WorkspaceManager (Swift):
- Reads
@mori-agent-stateand@mori-agent-nameduring 5s tmux poll - Maps hook states (
"working"→.running,"done"→.completed) - Updates
RuntimeWindow.detectedAgentand window badges - Clears stale pane options when agent exits
TmuxBackend (Swift):
- New methods:
unsetPaneOption(),setWindowOption() - TmuxParser includes agent fields in pane format string
TmuxCommandRunner (Swift):
- Improved environment loading: checks inherited PATH first (instant), falls back to login shell with 10s timeout
- Prevents hanging on
.appbundle launch with minimal environment
Sources/Mori/App/AgentHookConfigurator.swift— Hook install/uninstall logicSources/Mori/App/WorkspaceManager.swift— Hook state reading and cleanupSources/Mori/App/NotificationManager.swift— Agent name in notificationsSources/Mori/Resources/mori-*.sh— Hook scriptsSources/Mori/Resources/mori-pi-extension.ts— Pi extensionPackages/MoriCore/Models/RuntimeWindow.swift— AddeddetectedAgentfieldPackages/MoriTmux/Sources/MoriTmux/TmuxBackend.swift— Pane option getters/settersPackages/MoriUI/Sources/MoriUI/GhosttySettingsView.swift— Agent Hooks settings tab
- Keymaps
- CLAUDE.md — Agent-Aware Tabs section