# ai-notify

`ai-notify` is a macOS Rust CLI that sends `terminal-notifier` alerts for Claude Code hooks, native Codex hooks, and
Codex's legacy `notify` callback. Keep notification delivery macOS-specific while keeping pure logic and tests
platform-independent. Run validation locally with the workspace's nightly Rust toolchain.

## Upstream Documentation

- OpenAI Codex CLI `notify` callback: <https://learn.chatgpt.com/docs/config-file/config-advanced#notifications>
- OpenAI Codex native hooks: <https://learn.chatgpt.com/docs/hooks>
- Claude Code hook configuration and event schemas: <https://code.claude.com/docs/en/hooks>

## Development Workflow

- Use the rolling nightly toolchain and lockfile declared in `toolkit/` for every Cargo build, test, and install.
- Run the package from `toolkit/` with `cargo run -p ai-notify --locked -- ...`.
- Prefer `cargo test -p ai-notify --locked` for focused verification and `just rust-check` for the aggregate Rust gate.
- Do not run `just install-cli` for ordinary verification. It installs every CLI supported on this OS under `~/.local`.

## Architecture and Invariants

- Commands under `ai-notify event` read hook JSON from stdin. `event codex` handles native `UserPromptSubmit` and
  `Stop`, tracking prompts under a `codex:<session_id>:<turn_id>` key in the existing SQLite schema. The legacy
  `ai-notify codex` callback accepts JSON as its final argument or via `--stdin` without creating a tracked SQLite
  session.
- Hook event commands (`ai-notify event ...` and the legacy `ai-notify codex` callback) never exit 2 on an invalid
  payload. Parse and validation failures there exit 1. Claude Code and Codex treat a hook's exit 2 as a blocking
  decision. For example, Stop would keep the agent going and PreToolUse would block the tool. Only clap CLI usage
  errors, a missing or blank `ai-notify codex` payload, and non-hook commands (`config`, `link`, `check`, `cleanup`,
  `test`) still use exit 2.
- `integrations::CODEX_HOOK_EVENTS` lists the native Codex events `check` requires for `ai-notify event codex`.
- `integrations::HOOK_SPECS` is the source of truth for installed Claude hooks. The integration inspector derives its
  required event set from that list so `link claude` and `check` stay aligned.
- Preserve unrelated settings and hooks when changing integration writers. `link codex` must continue to refuse a
  different root `notify` value unless forced. Profile names resolve to sibling `<profile>.config.toml` files.
- Configuration respects `XDG_CONFIG_HOME` and defaults to `~/.config/ai-notify`. `ConfigLoader` caches the loaded
  configuration for its own lifetime (one load per CLI invocation).
- Claude `Stop` defers completion while `background_tasks` or `session_crons` are present. `StopFailure` alerts only in
  `all` mode and bypasses duration and prompt filters. Both Codex integrations omit duration filtering, suppress
  internal title-generation prompts, and apply notification mode and prompt-prefix exclusions.
- SQLite uses WAL mode with `synchronous=NORMAL`. Session data is intentionally transient rather than strictly durable.

## Testing

- Keep focused unit tests beside their Rust modules and CLI contract tests in `tests/cli.rs`. Use
  `cargo test -p ai-notify --locked <filter>` for targeted verification.
- Isolate configuration and database paths with temporary directories. Tests must not write to the user's actual XDG
  configuration directory.
- Inject or mock the macOS platform check, `terminal-notifier` discovery, and subprocess calls. Tests must not require
  the real notifier or send desktop alerts.
- For hook or Codex configuration changes, cover idempotence, preservation of unrelated configuration, and conflict
  behavior.

## CLI reference

ai-notify is a desktop notification system for Claude Code and Codex CLI. It tracks Claude Code session activity and
sends macOS notifications for key events.

![ai-notify notification demo](demo.png)

### Installation

> [!NOTE]
>
> This CLI works only on macOS at the moment.

#### Prerequisites

- Rust nightly (including `rustfmt` and Clippy)
- [terminal-notifier](https://github.com/julienXX/terminal-notifier) app: Install with `brew install terminal-notifier`
- (optional) [it2](https://github.com/mkusaka/it2) CLI with iTerm2's Python API enabled
  (`iTerm2 > Settings > General > Magic > Enable Python API`): clicking a notification can focus the exact iTerm2
  session that produced it, instead of only activating the app

#### Installation

Install the CLI directly from the agent-skills repository:

```bash
cargo install --git https://github.com/PaulRBerg/agent-skills ai-notify --locked --root "$HOME/.local"
```

Re-run the command to update. Installation targets `~/.local/bin`. Configure integrations separately with
`ai-notify link claude` or `ai-notify link codex`.

### Development

Use the locked dependency graph for local commands:

```bash
cargo build -p ai-notify --locked
cargo test -p ai-notify --locked
cargo run -p ai-notify --locked -- --help
```

Run `just rust-check` from `toolkit/` for the complete Rust workspace gate.

### Features

- **Smart Notifications**: ai-notify filters Claude Code completion alerts by a configurable duration threshold
  (default: 10s).
- **Prompt Filtering**: Exclude specific prompt patterns, such as slash commands like `/commit`, from notifications.
- **Session Tracking**: The SQLite database tracks Claude Code prompts, durations, and job numbers.
- **Auto-cleanup**: ai-notify cleans data automatically, with optional export before deletion.
- **Event Handlers**: CLI subcommands handle Claude Code hooks and Codex CLI notify integration.
- **Configuration**: ai-notify uses YAML configuration with defaults.
- **Context-rich Notifications**: Notifications include project, agent status, task, and result/action excerpts with
  custom agent icons.

#### Comparison with CCNotify

ai-notify was inspired by [CCNotify](https://github.com/dazuiba/CCNotify). It differs in these ways:

- **Independent CLI** — ai-notify runs as a standalone Rust binary instead of a script symlink.
- **Fully configurable** — ai-notify uses YAML config with `ai-notify config` commands.
- **More events** — ai-notify adds `PermissionRequest`, `StopFailure`, and an AskUserQuestion notifier (6 hooks vs 3).
- **Smart filtering** — ai-notify provides a configurable duration threshold and prompt exclusion patterns.
- **Notification modes** — ai-notify supports all/permission_only/disabled.

### Configuration

ai-notify uses YAML configuration stored under `$XDG_CONFIG_HOME/ai-notify`, defaulting to
`~/.config/ai-notify/config.yaml` when `XDG_CONFIG_HOME` is unset.

#### View current configuration

```bash
ai-notify config show
```

#### Edit configuration

```bash
ai-notify config edit
```

#### Reset to defaults

```bash
ai-notify config reset
```

#### Configuration options

```yaml
cleanup:
  auto_cleanup_enabled: true # Enable automatic cleanup of old data
  export_before_cleanup: true # Export data before cleanup
  retention_days: 30 # Number of days to retain session data (older data will be auto-cleaned)

database:
  path: ~/.config/ai-notify/ai-notify.db # Path to SQLite database file

logging:
  level: INFO # Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
  path: ~/.config/ai-notify/ai-notify.log # Path to log file

notification:
  app_bundle: com.googlecode.iterm2 # Application bundle ID to focus on notification click
  mode: all # Notification mode: 'all' (default), 'permission_only', or 'disabled'
  sound: default # Notification sound (see /System/Library/Sounds for options)
  threshold_seconds: 10 # Minimum job duration in seconds to trigger notification (0 = notify all)
  exclude_patterns: # List of prompt prefixes to exclude from notifications (case-sensitive)
    - /commit
    - /update-pr
    - /fix-issue
```

**Database durability note**

ai-notify uses SQLite WAL mode with `synchronous=NORMAL` for speed. ai-notify can lose the latest session writes after a
sudden power loss or crash. This is acceptable for transient session tracking data, but consider it if you require
strict durability.

**Prompt Pattern Filtering**

The `exclude_patterns` configuration lets you exclude notifications for specific prompts:

- **Prefix matching**: Patterns match prompts that _start with_ the pattern (case-sensitive)
- **Common use case**: Exclude slash commands like `/commit`, `/update-pr`, etc.
- **Examples**:
  - Pattern `/commit` matches: `/commit`, `/commit --all`, `/commit -m "message"`
  - Pattern `/commit` does NOT match: `Commit changes`, `run /commit`, `/Commit` (different case)
- **Default**: Empty list (no filtering)

### Usage

#### Test notifications

```bash
ai-notify test
```

#### Clean up old data

```bash
# Preview cleanup (dry run)
ai-notify cleanup --dry-run

# Clean up with confirmation
ai-notify cleanup

# Clean up with custom retention
ai-notify cleanup --days 60

# Clean up without export
ai-notify cleanup --no-export
```

#### Event handlers (Claude Code integration)

The following commands support Claude Code hooks:

```bash
# Track user prompt submission
ai-notify event user-prompt-submit < event_data.json

# Handle stop event (sends notifications)
ai-notify event stop < event_data.json

# Handle API failure event (sends failure notifications)
ai-notify event stop-failure < event_data.json

# Handle notification event
ai-notify event notification < event_data.json

# Handle permission request
ai-notify event permission-request < event_data.json

# Notify when Claude asks a question (PreToolUse / AskUserQuestion)
ai-notify event ask-user-question < event_data.json
```

For Codex CLI, use `ai-notify codex` via the `notify` setting (see below).

### Claude Code Hook Integration

Claude Code reads hooks from its settings files — `~/.claude/settings.json` (all projects) or a project's
`.claude/settings.json` / `.claude/settings.local.json`. Install the ai-notify hooks automatically:

```bash
ai-notify link claude
```

This merges the hooks into `~/.claude/settings.json` without changing your other settings. Use `--path` to target a
project or local settings file. Use `--dry-run` to preview. Use `--force` to replace a conflicting entry. For more
information about Claude Code hooks, see the [official documentation](https://code.claude.com/docs/en/hooks).

If a `settings.json` has a sibling `settings/hooks.jsonc`, ai-notify treats it as generated and refuses to overwrite it,
including when that file is selected with `--path`. Update `settings/hooks.jsonc` through its owning configuration and
run that configuration's normal settings generator to regenerate `settings.json`.

The resulting `hooks` section uses Claude Code's nested schema (each event maps to a list of matcher groups):

```json
{
  "hooks": {
    "UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "ai-notify event user-prompt-submit" }] }],
    "Stop": [{ "hooks": [{ "type": "command", "command": "ai-notify event stop" }] }],
    "StopFailure": [{ "hooks": [{ "type": "command", "command": "ai-notify event stop-failure" }] }],
    "Notification": [{ "hooks": [{ "type": "command", "command": "ai-notify event notification" }] }],
    "PermissionRequest": [{ "hooks": [{ "type": "command", "command": "ai-notify event permission-request" }] }],
    "PreToolUse": [
      {
        "matcher": "AskUserQuestion",
        "hooks": [{ "type": "command", "command": "ai-notify event ask-user-question" }]
      }
    ]
  }
}
```

> **Upgrading:** Claude Code does not read standalone `~/.claude/hooks/hooks.json` or global
> `~/.claude/settings.local.json` files. Re-run `ai-notify link claude` to install into `~/.claude/settings.json`, then
> remove stale hook configuration from those ignored locations. `ai-notify check` reports them when present.

Claude Code fires `StopFailure` instead of `Stop` when an API error ends a turn. ai-notify marks the tracked prompt
failed and, in `all` mode, alerts immediately without applying the duration threshold or prompt exclusions. Normal
`Stop` events do not complete the tracked prompt while the payload lists background tasks or session crons. A user
interrupt fires neither `Stop` nor `StopFailure`, so it cannot produce a completion notification.

### Codex CLI Integration

#### Native hooks

For Codex versions supporting native hooks, merge these handlers into `~/.codex/hooks.json`, preserving existing hooks:

```json
{
  "hooks": {
    "UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "ai-notify event codex", "timeout": 5 }] }],
    "Stop": [{ "hooks": [{ "type": "command", "command": "ai-notify event codex", "timeout": 5 }] }]
  }
}
```

Review and trust the two exact definitions through Codex's `/hooks` interface. Remove ai-notify from the legacy `notify`
callback to prevent duplicate notifications. If Codex Desktop owns a `SkyComputerUseClient` wrapper, preserve the
wrapper and remove only its ai-notify `--previous-notify` forwarding arguments.

`UserPromptSubmit` retains task context for `Stop`. ai-notify ignores a repeated stop for a tracked turn. If no prompt
was recorded, such as when hooks are installed during a turn, the completion still reports the available result. Native
hooks preserve the Codex notification mode and prompt-prefix filters and do not apply `threshold_seconds`.

Codex 0.155.1 disables native hooks in internal title-generation threads, so those threads never invoke these handlers.
See the [native hook contract](https://learn.chatgpt.com/docs/hooks). Use `/hooks` to review trust for this integration.
`ai-notify check` reports these handlers from `~/.codex/hooks.json`, and `ai-notify link codex` configures only the
legacy callback described below.

#### Legacy notify callback

Codex CLI can invoke ai-notify through the `notify` setting in your root config file (`~/.codex/config.toml`).

```toml
notify = ["ai-notify", "codex"]
```

You can also set this automatically:

```bash
ai-notify link codex
```

An existing different root `notify` command is left unchanged and causes a nonzero exit. Review the reported previous
value, then replace it explicitly if intended:

```bash
ai-notify link codex --force
```

`ai-notify check` accepts an argv array whose first program has the basename `ai-notify` and whose later arguments
include the standalone argument `codex`. It also accepts the same argv shape in a whitespace-delimited string, and the
Codex Desktop `SkyComputerUseClient` wrapper when its `--previous-notify` value is exactly `["ai-notify", "codex"]`.
`ai-notify link codex` writes the canonical array shown above. Without `--force`, other existing representations remain
conflicts, except for that exact Codex Desktop wrapper.

Codex profiles are separate files next to the base config, not `[profiles.<name>]` tables. To configure
`~/.codex/review.config.toml`:

```bash
ai-notify link codex --profile review
```

Profile names may contain letters, numbers, hyphens, and underscores. The profile file overlays the base
`~/.codex/config.toml`, so it only needs a `notify` value when it should override the base. See the official
[profile](https://developers.openai.com/codex/config-advanced#profiles) and
[notification](https://developers.openai.com/codex/config-advanced#notifications) documentation.

Codex appends the JSON payload as the final CLI argument, so the effective command looks like:

```bash
ai-notify codex '<json payload>'
```

The Codex notify payload does not include job duration, so `notification.threshold_seconds` is not applied for Codex
notifications. Exclude patterns and notification mode still apply. ai-notify uses specific prompt envelopes to suppress
internal automatic-title and rename-suggestion requests. Ordinary user requests to write titles still notify.

Codex 0.155.1 runs these requests in hidden ephemeral threads that disable `features.hooks` but inherit the legacy
`notify` command. Its callback carries no ephemeral/thread-source marker, so ai-notify recognizes the two prompts in
[Codex's title generator](https://github.com/openai/codex/blob/rust-v0.155.1/codex-rs/tui/src/app/thread_title.rs).

### Integration Check

Use the built-in checker to see whether Claude Code hooks and Codex notifications are configured:

```bash
ai-notify check

# Check the effective base + profile configuration
ai-notify check --profile review
```

Codex is OK when either mechanism is fully configured: `~/.codex/hooks.json` has `ai-notify event codex` command
handlers for both `UserPromptSubmit` and `Stop` (any matcher, since Codex ignores it for those events), or the effective
legacy `notify` runs `ai-notify codex`. The output lists each contributing source and any missing native hook event. A
malformed `hooks.json` or Codex config, or a missing profile file, reports `ERROR` and exits 1. Inline `[hooks]` tables
and project `.codex/hooks.json` files are not inspected.

### How It Works

1. **UserPromptSubmit**: When you submit a prompt to Claude Code, ai-notify tracks it in the database.
2. **Stop**: When Claude finishes with no pending background tasks or session crons, ai-notify:
   - Calculates the duration.
   - Checks if duration >= threshold.
   - Checks if the prompt starts with any excluded pattern.
   - Sends the task and Claude's final response when both checks pass (by default, when the job took at least 10
     seconds).
   - Optionally runs auto-cleanup (every 24 hours).
3. **StopFailure**: When an API error ends the turn, ai-notify marks the prompt stopped and sends the task, duration,
   and documented error text in `all` mode, even for short or excluded prompts. If the prompt was not tracked, ai-notify
   sends a generic failure notification.
4. **Notification**: ai-notify detects "waiting for input" notifications (via the `idle_prompt` notification type, with
   a keyword fallback) and suppresses them. The Stop handler sends the job-completion notification.
5. **PermissionRequest**: ai-notify sends the current task and the command, path, URL, query, or description that needs
   approval.
6. **PreToolUse / AskUserQuestion**: ai-notify sends the current task, first question, and count of any remaining
   questions.

Notifications use the project as the title and the agent state as the subtitle. Prompt sequence numbers remain in the
transient session database but are not displayed. Task excerpts are capped at 100 characters and result, error, request,
and question excerpts at 180 characters.

For Codex CLI, ai-notify runs on the `agent-turn-complete` notify event and sends the latest user message and assistant
message using the same layout. Codex payloads do not provide turn duration.

### Runtime Files

When `XDG_CONFIG_HOME` is unset, ai-notify uses these default locations:

```
~/.config/ai-notify/
├── config.yaml             # User configuration
├── ai-notify.db            # SQLite session database
├── ai-notify.log           # Application logs
└── exports/                # JSON exports before cleanup
```

ai-notify initializes fresh and version-0 session databases at schema version 1. Commands that use session state reject
a newer schema with operational exit code 1 and leave the database unchanged. In that case, move or remove the reported
database file before retrying with this ai-notify version.

### Documentation

- [Claude Code](https://code.claude.com/docs/en/overview)
- [Claude Code hooks](https://code.claude.com/docs/en/hooks)
- [Codex CLI advanced configuration](https://developers.openai.com/codex/config-advanced)

### Contributing

Follow the development workflow and repository-specific constraints above.

### License

MIT License - see [LICENSE.md](../LICENSE.md)
