---
name: seelie-gateway-setup
description: |
  Install the @eastlake/seelie-gateway CLI and configure AI coding tools
  to forward lifecycle events to the Seelie desktop pet over UDP.
  Use when the user wants to "set up the gateway", "connect Seelie",
  "hook up Codex/Claude/Kimi/OpenCode to Seelie", "make the pet react",
  "install seelie-gateway", or mentions Seelie + any AI tool integration.
license: MIT
metadata:
  author: seelie
  version: "2.0"
---

# Seelie Gateway Setup

Connect AI coding tools to the Seelie desktop pet. The gateway translates tool lifecycle events into UDP datagrams that Seelie listens for on `127.0.0.1:52847`.

## When to use

- "install/setup the gateway", "connect Seelie to <tool>"
- "make the pet react to <tool>", "hook up <tool>"
- debugging why Seelie doesn't react to a specific tool
- adding gateway support for a new tool
- user mentions hooks for Claude Code, Codex CLI, Kimi-CLI, OpenCode, or any AI coding tool

## Overview

```
AI Tool ──hook──▶ seelie-gateway ──UDP──▶ Seelie pet (127.0.0.1:52847)
```

The gateway normalizes each tool's native events into 17 canonical event names. Seelie's `EventRouter` maps these to animations and effects.

## Step 1: Install the gateway

From npm:

```sh
npm i -g @eastlake/seelie-gateway
```

Verify Seelie is listening:

```sh
seelie-gateway --ping   # → "Seelie is alive" if the pet is running
```

## Step 2: Create the PATH wrapper (CRITICAL)

> **CRITICAL**: Most AI tools spawn hooks with a minimal shell environment — no fnm/nvm shims, no user shell rc. A bare `seelie-gateway` command works in your terminal but **silently fails** from hooks.

Save this as `~/.local/bin/seelie-gateway-hook` and `chmod +x` it:

```sh
#!/bin/sh
set -eu
# Resolve the gateway binary through the user's Node version manager.
# Adapt the path for the user's setup (fnm / nvm / system Node).

# --- fnm (macOS) ---
fnm_root="$HOME/Library/Application Support/fnm/node-versions"
# --- fnm (Linux) ---
# fnm_root="$HOME/.local/share/fnm/node-versions"

selected_base=""
for base in "$fnm_root"/*/installation; do
  [ -d "$base" ] || continue
  selected_base="$base"
done
[ -z "$selected_base" ] && { echo "no fnm installation found" >&2; exit 127; }

node_bin="$selected_base/bin/node"
gateway_cli="$selected_base/lib/node_modules/@eastlake/seelie-gateway/cli.mjs"

[ -x "$node_bin" ]    || { echo "node not found at $node_bin" >&2; exit 127; }
[ -f "$gateway_cli" ] || { echo "gateway not found at $gateway_cli" >&2; exit 127; }

exec "$node_bin" "$gateway_cli" "$@"
```

**For nvm users** — replace the fnm block with:
```sh
node_bin="$HOME/.nvm/versions/node/<VERSION>/bin/node"
gateway_cli="$HOME/.nvm/versions/node/<VERSION>/lib/node_modules/@eastlake/seelie-gateway/cli.mjs"
```

**For system Node** — use `which node` for `node_bin` and `npm root -g` to locate `gateway_cli`.

All hook configs below reference `~/.local/bin/seelie-gateway-hook` instead of bare `seelie-gateway`.

## Step 3: Detect the user's platform

Before writing any hook config, determine the user's OS and adjust paths:

| Platform | Home dir | Wrapper path |
|----------|----------|-------------|
| macOS | `/Users/<name>` | `/Users/<name>/.local/bin/seelie-gateway-hook` |
| Linux | `/home/<name>` | `/home/<name>/.local/bin/seelie-gateway-hook` |
| Windows | `C:\Users\<name>` | `C:\Users\<name>\.local\bin\seelie-gateway-hook.cmd` |

All examples below use `<HOME>/.local/bin/seelie-gateway-hook` — replace `<HOME>` with the detected path.

## Step 4: Configure the user's AI tool

Ask the user which tool(s) they use, then apply the matching config below.

### Claude Code

Edit `~/.claude/settings.json` (merge with existing):

```json
{
  "hooks": {
    "SessionStart": [{
      "hooks": [{
        "type": "command",
        "command": "<HOME>/.local/bin/seelie-gateway-hook --source claude-code --event session.start"
      }]
    }],
    "PreToolUse": [{
      "hooks": [{
        "type": "command",
        "command": "<HOME>/.local/bin/seelie-gateway-hook --source claude-code --event tool.before"
      }]
    }],
    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit|NotebookEdit",
        "hooks": [{
          "type": "command",
          "command": "<HOME>/.local/bin/seelie-gateway-hook --source claude-code --event file.edited"
        }]
      },
      {
        "hooks": [{
          "type": "command",
          "command": "<HOME>/.local/bin/seelie-gateway-hook --source claude-code --event tool.after"
        }]
      }
    ],
    "Notification": [{
      "hooks": [{
        "type": "command",
        "command": "<HOME>/.local/bin/seelie-gateway-hook --source claude-code --event notification.sent"
      }]
    }],
    "Stop": [{
      "hooks": [{
        "type": "command",
        "command": "<HOME>/.local/bin/seelie-gateway-hook --source claude-code --event session.end"
      }]
    }]
  }
}
```

Session ID is auto-detected from `~/.claude/sessions/<ppid>.json`.

### OpenCode

OpenCode uses JS plugins instead of shell hooks. Save as `~/.config/opencode/plugins/seelie.mjs`:

```js
import { spawn } from 'node:child_process';

const HOOK = '<HOME>/.local/bin/seelie-gateway-hook';

// OpenCode bus event → Seelie canonical event
const EVENT_MAP = {
  'session.created':       'session.start',
  'session.idle':          'session.idle',
  'session.error':         'session.error',
  'message.user.created':  'prompt.submitted',
  'tool.execute.before':   'tool.before',
  'tool.execute.after':    'tool.after',
  'permission.requested':  'permission.requested',
  'permission.replied':    'permission.response',
  'file.edited':           'file.edited',
  'file.watcher.updated':  'file.watched',
};

export const SeeliePlugin = async () => ({
  event: async ({ event }) => {
    const mapped = EVENT_MAP[event.type];
    if (!mapped) return;
    spawn(HOOK, ['--source', 'opencode', '--event', mapped], {
      stdio: 'ignore',
      detached: true,
    }).unref();
  },
});
```

### Codex CLI

1. Enable hooks in `~/.codex/config.toml`:

```toml
[features]
codex_hooks = true
```

2. Write `~/.codex/hooks.json` (**lowercase** `"command"` — capital C is silently ignored):

```json
{
  "hooks": {
    "SessionStart":      [{"hooks":[{"type":"command","command":"<HOME>/.local/bin/seelie-gateway-hook --source codex --event session.start","timeout":5}]}],
    "UserPromptSubmit":  [{"hooks":[{"type":"command","command":"<HOME>/.local/bin/seelie-gateway-hook --source codex --event prompt.submitted","timeout":5}]}],
    "PreToolUse":        [{"hooks":[{"type":"command","command":"<HOME>/.local/bin/seelie-gateway-hook --source codex --event tool.before","timeout":5}]}],
    "PostToolUse":       [{"hooks":[{"type":"command","command":"<HOME>/.local/bin/seelie-gateway-hook --source codex --event tool.after","timeout":5}]}],
    "PermissionRequest": [{"hooks":[{"type":"command","command":"<HOME>/.local/bin/seelie-gateway-hook --source codex --event permission.requested","timeout":5}]}],
    "Stop":              [{"hooks":[{"type":"command","command":"<HOME>/.local/bin/seelie-gateway-hook --source codex --event session.end","timeout":5}]}]
  }
}
```

Session ID is auto-detected from `~/.codex/session_index.jsonl`.

### Kimi-CLI

Hooks live in `~/.kimi/config.toml`. Append these `[[hooks]]` tables:

```toml
[[hooks]]
event = "SessionStart"
command = "<HOME>/.local/bin/seelie-gateway-hook --source kimi-cli --event session.start"
timeout = 5

[[hooks]]
event = "UserPromptSubmit"
command = "<HOME>/.local/bin/seelie-gateway-hook --source kimi-cli --event prompt.submitted"
timeout = 5

[[hooks]]
event = "PreToolUse"
command = "<HOME>/.local/bin/seelie-gateway-hook --source kimi-cli --event tool.before"
timeout = 5

[[hooks]]
event = "PostToolUse"
command = "<HOME>/.local/bin/seelie-gateway-hook --source kimi-cli --event tool.after"
timeout = 5

[[hooks]]
event = "PostToolUseFailure"
command = "<HOME>/.local/bin/seelie-gateway-hook --source kimi-cli --event tool.failed"
timeout = 5

[[hooks]]
event = "Notification"
command = "<HOME>/.local/bin/seelie-gateway-hook --source kimi-cli --event notification.sent"
timeout = 5

[[hooks]]
event = "SessionEnd"
command = "<HOME>/.local/bin/seelie-gateway-hook --source kimi-cli --event session.end"
timeout = 5
```

### Any other tool

If the tool can run a shell command on lifecycle events:

1. Pick a `--source <slug>` name for the tool.
2. Map each native event to the closest canonical event (see table below).
3. Invoke: `<HOME>/.local/bin/seelie-gateway-hook --source <slug> --event <canonical>`.
4. Optional flags: `--tool-name Read`, `--file-path <path>`, `--session <id>`, `--title "..."`, `--animation wave`.

If the tool only supports JS/Python plugins, spawn the wrapper as a detached child process (see OpenCode example).

## Canonical event vocabulary

The gateway accepts exactly 17 event names via `--event`:

| Category | Events |
|----------|--------|
| Session | `session.start` `session.end` `session.idle` `session.error` |
| Prompt | `prompt.submitted` |
| Tool | `tool.before` `tool.after` `tool.failed` |
| Permission | `permission.requested` `permission.denied` `permission.response` |
| Subagent | `subagent.started` `subagent.stopped` |
| Notification | `notification.sent` |
| File | `file.edited` `file.watched` `todo.updated` |

Every event name must match exactly. Unknown names are silently ignored.

## Step 5: Verify end-to-end

With Seelie running:

```sh
# Basic connectivity
seelie-gateway --ping

# Test a single event — pet should react
seelie-gateway --source claude-code --event session.start

# Test the wrapper script
~/.local/bin/seelie-gateway-hook --source codex --event tool.before --tool-name Read
```

Watch the pet. Every event maps to an animation via `EventRouter`. If no reaction:
1. Confirm the event name is one of the 17 canonical names.
2. Confirm Seelie is running and listening on `127.0.0.1:52847`.
3. Check the wrapper script path is correct and executable.

## Pitfalls

1. **Capital `"Command"` in Codex hooks.json** — silently ignored. Must be lowercase `"command"`.
2. **Bare `seelie-gateway` in hooks** — works in terminals (fnm shim on PATH), fails silently from hooks. Always use the wrapper script.
3. **Timeout too long** — Codex and Kimi block on hooks up to the timeout. Keep ≤ 5 s; UDP send takes < 10 ms.
4. **Missing `codex_hooks = true`** — Codex silently ignores `hooks.json` without the feature flag in `config.toml`.
5. **fnm + `npm link` + version switch** — the global symlink lives under one Node version. Re-link or hard-code the version path in the wrapper.
6. **OpenCode plugin not detached** — without `detached: true` + `.unref()`, the plugin holds a handle on the child process and can hang shutdown.
