# Custom Agents: integration guide (Beta)

This is the complete guide to showing an AI agent you built in the Vibe Island notch. It is written so a coding agent can follow it step by step. The integration is one shell command per event: no SDK, no hooks system needed, and the command never fails your agent (it exits 0 whether or not Vibe Island is running and does not wait for a reply). Beta: needs Vibe Island 1.0.53 or newer, direct download edition.

## How to run this guide

Do the work yourself, in the user's project: edit the agent's own code or hook configuration (Vibe Island never edits it for you), and never just hand the user a list of commands. Stop and ask the user only at the points marked "Ask the user" or "Stop and tell the user"; otherwise keep going. Never edit anything under `~/.vibe-island/` except the one optional manifest file in Step 5. Every step ends with "Check passed when ..."; do not move on before it holds. When you are done, print the report from Step 7.

## Try it by hand

Pick a name for your agent (lowercase letters, digits, `-` or `_`; the examples use `acme-coder`), make sure Vibe Island is running, and run these one at a time. Move the pointer to the notch to open the panel.

```sh
echo '{"hook_event_name":"SessionStart","session_id":"demo-1","cwd":"/tmp"}' | ~/.vibe-island/bin/vibe-island-bridge --source acme-coder
```

A card for `tmp` appears, carrying your agent's name (as a text tag, or as an `A` badge you can hover).

```sh
echo '{"hook_event_name":"Stop","session_id":"demo-1","cwd":"/tmp","last_assistant_message":"Done."}' | ~/.vibe-island/bin/vibe-island-bridge --source acme-coder
```

The card shows the completion notice `Done.`

```sh
echo '{"hook_event_name":"SessionEnd","session_id":"demo-1","cwd":"/tmp"}' | ~/.vibe-island/bin/vibe-island-bridge --source acme-coder
```

The card goes away. Your agent sends the same three events when a session opens, a turn ends and the session closes.

## Procedure

### Step 1: Check the preconditions

```sh
# Edition: a Mac App Store install carries a receipt.
[ -e "/Applications/Vibe Island.app/Contents/_MASReceipt/receipt" ] && echo "edition: Mac App Store" || echo "edition: direct download"

# The command exists and is executable.
BRIDGE="$HOME/.vibe-island/bin/vibe-island-bridge"
[ -x "$BRIDGE" ] && echo "command: ok" || echo "command: MISSING"

# Vibe Island is running.
pgrep -x vibe-island >/dev/null && echo "app: running" || echo "app: NOT RUNNING"

# Installed version (works when the app is in /Applications).
echo "version: $(defaults read "/Applications/Vibe Island.app/Contents/Info" CFBundleShortVersionString 2>/dev/null || echo unknown)"
```

| Result | Stop and tell the user |
|---|---|
| `edition: Mac App Store` | Custom agents are not supported in the Mac App Store edition yet. |
| `command: MISSING` | Vibe Island is not installed or was never opened. Install the latest version from vibeisland.app and open it once. After Remove All Auto-Configuration (Settings > About), turn an integration back on in Settings > Integrations. |
| `app: NOT RUNNING` | Open Vibe Island. Events sent while it is closed are dropped. |
| `version:` below `1.0.53` | Update Vibe Island to 1.0.53 or newer. If it says `unknown`, ask which version is installed. |

The edition and version checks assume the app is at `/Applications/Vibe Island.app`. If it is installed elsewhere, ask the user for the version and edition instead of guessing.

Check passed when the edition is the direct download, the command is executable, the app is running, and the version is 1.0.53 or newer.

### Step 2: Choose the source name

The source name is your agent's identity: the value of `--source`, the file name of the optional manifest, and the prefix of every session id. Pick it from the agent's product name (or keep the user's own name for it) and keep it for good.

- It matches `[a-z][a-z0-9_-]{0,63}`: lowercase letters, digits, `_` and `-`, starting with a letter.
- It is not a built-in agent's name. The check below lists them; the list grows, so put your product or company in the name.
- It has no `_custom_` in it: a built-in name plus `_custom_`, such as `claude_custom_0`, is reserved.
- Prefer 12 characters or fewer: a longer name can send a click to the wrong tab in Ghostty, see [Jumping back to your app](#jumping-back-to-your-app).

Good: `acme-coder`, `lint_bot`. Bad: `claude` (handled as that built-in agent), `Acme Coder` (uppercase, a space; use the manifest's display name), `2fast` (starts with a digit).

```sh
SOURCE="acme-coder"
printf '%s' "$SOURCE" | grep -Eq '^[a-z][a-z0-9_-]{0,63}$' && echo "format: ok" || echo "format: INVALID"
case "$SOURCE" in
  claude|codex|zcode|gemini|antigravity|cursor|trae|opencode|mimocode|devin|droid|qoder|qodercn|qoderwork|qwenwork|qwen|grok|copilot|vscodeagent|codebuddy|workbuddy|kiro|kimi|kimicode|deepseek|dsh|mistralvibe|gajaecode|hermes|amp|pi|ohmypi|openclaw|*_custom_*)
    echo "name: RESERVED" ;;
  *) echo "name: ok" ;;
esac
```

If the product is a fork of a built-in agent that only moved its config folder and kept its hook protocol, this guide is the wrong tool: add the folder in Settings > Integrations > CLI Hooks instead.

Check passed when both lines say `ok`.

### Step 3: Decide where to hook in

Your agent does not need a hooks system: run the command from wherever your code knows the event happened, whether that is a hook, a callback, an event handler, or a wrapper script around your agent. Map the moments in its lifecycle to events. The smallest set that works is `SessionStart` plus `Stop`.

| Moment in your agent | Event | What the user sees |
|---|---|---|
| A session opens | `SessionStart` | The card appears, idle. Minimum set. |
| The user sends a message | `UserPromptSubmit` | Working; the prompt is the summary. |
| A tool call starts or ends | `PreToolUse`, `PostToolUse` | Working, with the tool name. |
| The agent needs an approval | `Notification` with `"notification_type":"permission_prompt"` | A read-only reminder, see [Approval reminders](#approval-reminders). |
| The turn finishes | `Stop` | Idle, and a completion notice with the reply if you send `last_assistant_message`. Minimum set. |
| The turn fails | `StopFailure` | Idle. |
| The session closes | `SessionEnd` | The card is removed. |

Call the command from the agent's own process, in its own terminal environment: from a detached background service it still reports status, but a click on the card cannot return to the agent's terminal. The agent must run as a local process; a packaged desktop app counts, a page in a browser tab does not.

Check passed when you can name the code location for `SessionStart`, `Stop` and `SessionEnd`.

### Step 4: Send events

Replace `acme-coder` with your source name and `demo-1` with your session id. Put `cwd` in every event: after Vibe Island restarts, the next event of a running session brings its card back, and `cwd` names it.

```sh
# Session opened
echo '{"hook_event_name":"SessionStart","session_id":"demo-1","cwd":"/tmp"}' | ~/.vibe-island/bin/vibe-island-bridge --source acme-coder

# User sent a message
echo '{"hook_event_name":"UserPromptSubmit","session_id":"demo-1","cwd":"/tmp","prompt":"Fix the failing login test"}' | ~/.vibe-island/bin/vibe-island-bridge --source acme-coder

# Tool call started
echo '{"hook_event_name":"PreToolUse","session_id":"demo-1","cwd":"/tmp","tool_name":"Bash","tool_input":{"command":"npm test"}}' | ~/.vibe-island/bin/vibe-island-bridge --source acme-coder

# Tool call ended
echo '{"hook_event_name":"PostToolUse","session_id":"demo-1","cwd":"/tmp","tool_name":"Bash"}' | ~/.vibe-island/bin/vibe-island-bridge --source acme-coder

# Turn finished
echo '{"hook_event_name":"Stop","session_id":"demo-1","cwd":"/tmp","last_assistant_message":"Fixed the login test."}' | ~/.vibe-island/bin/vibe-island-bridge --source acme-coder

# Turn failed
echo '{"hook_event_name":"StopFailure","session_id":"demo-1","cwd":"/tmp","last_assistant_message":"The model API returned an error."}' | ~/.vibe-island/bin/vibe-island-bridge --source acme-coder

# Needs approval (read-only reminder)
echo '{"hook_event_name":"Notification","session_id":"demo-1","cwd":"/tmp","notification_type":"permission_prompt","tool_name":"Bash","tool_input":{"command":"rm -rf build"}}' | ~/.vibe-island/bin/vibe-island-bridge --source acme-coder

# Session closed
echo '{"hook_event_name":"SessionEnd","session_id":"demo-1","cwd":"/tmp"}' | ~/.vibe-island/bin/vibe-island-bridge --source acme-coder
```

In the agent's code, send events through one helper that builds the JSON with a serializer, sends each session's events in order, and gives the command a short timeout. This Node.js helper does that; port it to the project's language.

```js
// Node.js 18 or newer. Events are sent one at a time, in order, and never throw.
import { spawn } from "node:child_process";
import os from "node:os";
import path from "node:path";

const BRIDGE = path.join(os.homedir(), ".vibe-island", "bin", "vibe-island-bridge");
const SOURCE = "acme-coder";
let queue = Promise.resolve();

function send(event) {
  return new Promise((resolve) => {
    try {
      const child = spawn(BRIDGE, ["--source", SOURCE], { stdio: ["pipe", "ignore", "ignore"] });
      const timer = setTimeout(() => child.kill(), 2000);
      const done = () => { clearTimeout(timer); resolve(); };
      child.on("error", done);
      child.on("exit", done);
      child.stdin.on("error", () => {});
      child.stdin.end(JSON.stringify(event));
    } catch {
      resolve();
    }
  });
}

export function emit(event) {
  queue = queue.then(() => send(event));
}

emit({ hook_event_name: "SessionStart", session_id: "demo-1", cwd: process.cwd() });
```

Check passed when every event the agent can produce goes through the helper and a failure in it cannot stop the agent.

### Step 5: Add the manifest file (optional)

Skip this step unless you need a display name, a jump rule, custom event or field names, a custom stdout, or automatic turn end for a bot. Standard events need no file.

Path: `~/.vibe-island/integrations/<source>.json`. The command re-reads it on every call, and Vibe Island picks up changes within a few seconds, without restarting anything. The smallest useful file is `{"displayName": "Acme Coder"}`.

| Key | Type | Effect | Default |
|---|---|---|---|
| `displayName` | string | The name on the card: a text tag, or a first-letter badge with the full name on hover, per the user's display setting. | The source name, each word capitalized (`Acme-Coder`). |
| `stdoutResponse` | string | Printed to stdout on every call; an empty string prints an empty line. | `{}` |
| `fieldMap` | object of strings | Renames top-level payload fields, `{"yourName": "standard_name"}`. Applied first. | none |
| `eventMap` | object of strings | Translates event names, `{"your_event": "Stop"}`. Exact match, case included. Applied after `fieldMap`. | none |
| `terminatingTools` | array | Ends the turn when a tool call finishes, see below. | none |
| `bundleIdentifier`, `jumpRule` | string, object | Your app's bundle identifier and `{"method":"urlScheme","template":"..."}`, to land on the right conversation: [Jumping back to your app](#jumping-back-to-your-app). | none |

A key with the wrong type is ignored. A file that is not valid JSON, not an object, or larger than 1 MiB is ignored as a whole; the command then works with the defaults.

A manifest for an agent whose payload uses different names:

```json
{
  "displayName": "Acme Coder",
  "stdoutResponse": "",
  "fieldMap": {
    "event": "hook_event_name",
    "conversation_id": "session_id",
    "working_directory": "cwd"
  },
  "eventMap": {
    "task_completed": "Stop",
    "tool_call": "PreToolUse"
  },
  "terminatingTools": [
    "WeComReply",
    { "name": "SlackPostMessage", "when": { "is_final": true } }
  ]
}
```

With that file, `{"event":"task_completed","conversation_id":"c-1","working_directory":"/tmp"}` is delivered as a `Stop` event for session `c-1` in `/tmp`.

- `fieldMap` and `eventMap` see top-level keys only. If both the old and the new field are present, the old one wins.
- Standard spellings need no map. An event without `hook_event_name` after mapping is skipped.
- `terminatingTools` is for agents that never send `Stop`, such as a bot that calls a "send reply" tool and then waits. After a matching `PostToolUse` the command adds a `Stop`. A rule matches when `tool_name` equals `name` and every field in `when` (if present) equals the same top-level field of `tool_input`, compared by JSON type (`true` and `1` differ).

Check passed when the file parses (`python3 -m json.tool < ~/.vibe-island/integrations/acme-coder.json`) and is named exactly `<source>.json`.

### Step 6: Verify without seeing the notch

You cannot see the notch: verify through the command and the log, then ask the user.

The command prints `{}` (or your `stdoutResponse`) and exits 0 whether or not Vibe Island is running, so **exit 0 and `{}` do not prove delivery**; if the app cannot be found at all it prints nothing (Step 1 catches that). Each call also logs one JSON line to `~/Library/Logs/VibeIsland/bridge.log` with the keys `t` (UTC time), `s` (source name) and `m` (message). Key order varies, so match substrings.

**Ask the user** to watch the notch (moving the pointer to it opens the panel), then run this script. It sends a complete demo session, waits so the user can watch the card, closes it, and prints the log lines for your source.

```sh
#!/bin/sh
SOURCE="acme-coder"                         # your source name
BRIDGE="$HOME/.vibe-island/bin/vibe-island-bridge"
LOG="$HOME/Library/Logs/VibeIsland/bridge.log"
SID="verify-$(date +%s)"                    # a new id for every run
START=$(date -u +%Y-%m-%dT%H:%M:%SZ)

send() {
  out=$(printf '%s' "$1" | "$BRIDGE" --source "$SOURCE")
  echo "exit=$? stdout=$out"
}
entries() {
  grep "\"s\":\"$SOURCE\"" "$LOG" | awk -v start="$START" '
    match($0, /"t":"[^"]*"/) { t = substr($0, RSTART + 5, RLENGTH - 6); if (t >= start) print }'
}

echo "1/6 SessionStart";     send '{"hook_event_name":"SessionStart","session_id":"'"$SID"'","cwd":"/tmp"}'
sleep 3
echo "2/6 UserPromptSubmit"; send '{"hook_event_name":"UserPromptSubmit","session_id":"'"$SID"'","prompt":"Vibe Island integration check"}'
echo "3/6 PreToolUse";       send '{"hook_event_name":"PreToolUse","session_id":"'"$SID"'","tool_name":"Bash","tool_input":{"command":"echo check"}}'
sleep 3
echo "4/6 PostToolUse";      send '{"hook_event_name":"PostToolUse","session_id":"'"$SID"'","tool_name":"Bash"}'
echo "5/6 Stop";             send '{"hook_event_name":"Stop","session_id":"'"$SID"'","last_assistant_message":"Integration check finished."}'
sleep 8
echo "6/6 SessionEnd";       send '{"hook_event_name":"SessionEnd","session_id":"'"$SID"'"}'
sleep 1

PROBLEMS='connect failed|send failed|invalid JSON|missing hook_event_name|drop admission'
echo "--- log lines for $SOURCE since $START"
entries | grep -E "\"m\":\"event=|$PROBLEMS"
echo "--- events logged: $(entries | grep -c '"m":"event=') (expected 6)"
echo "--- problems: $(entries | grep -c -E "$PROBLEMS") (expected 0)"
```

| Log line contains | Meaning | What to do |
|---|---|---|
| `"m":"event=Stop"` | The event was recognized. Expect one line per call. | Nothing. |
| `connect failed` or `send failed` | Vibe Island was not running or not reachable; the event was dropped. A successful delivery writes no line in release builds, so no such line after an `event=` line is the signal. | Ask the user to open Vibe Island, then run the script again. |
| `invalid JSON` or `missing hook_event_name` | stdin was not a JSON object, or had no event name after mapping; the event was skipped. | Fix how the JSON is built, or add `hook_event_name` (or a `fieldMap` entry for it). |
| `drop admission hook` | The user blocked the app that launched the command. | Ask the user to remove it in Settings > Notifications > Blocked Launcher Apps. |

**Ask the user** to confirm, in this order:

1. A card for `tmp` with an `A` badge or an `Acme Coder` tag (or your display name) appeared, idle.
2. Three seconds later it turned to working, with the `Bash` tool and the summary `Vibe Island integration check`.
3. Three seconds after that it went idle with the completion notice `Integration check finished.`
4. Clicking the card (between the notice and the end of the script) brought the terminal window or app the agent runs in to the front.
5. Eight seconds after the notice the card disappeared.

No card although the log is clean: compare the log's `"s"` value with your source name, and check for a user rule hiding the card. A click in the wrong place: note which terminal or app the agent runs in and where it landed.

Check passed when the script prints six `event=` lines, zero problems and `exit=0` for every call (the events reached Vibe Island; no log line says what the notch displayed), and the user confirmed the card.

### Critical rules

1. `Stop` ends a turn, not the session. Send `SessionEnd` when the session closes, on every exit path you control; otherwise the card lingers until idle cleanup.
2. Use one unique id per session, the same on every event of that session, and make it a random UUID: ids that start with the same characters (`chat-1`, `chat-2`) can send a click to the wrong tab, see [Jumping back to your app](#jumping-back-to-your-app). Never reuse an id for a new conversation, and send nothing for an id after its `SessionEnd`.
3. Never block your agent on the command: 2-second timeout, ignore the result, send each session's events in order.
4. Ignore stdout; do not require `{}`.
5. Build the JSON with a serializer, never by joining strings.
6. Use your own source name, and send `Notification` only with `permission_prompt`; any other `Notification` returns the card to idle.

### Step 7: Report back

Print this block for the user, filled in:

```text
Vibe Island integration report
Source name:   acme-coder
Files changed: <paths>
Events wired:  <event: file and function, one per line>
Manifest:      <written at ~/.vibe-island/integrations/acme-coder.json | not needed>
Verification:  <n>/6 events logged, <n> problems
To confirm:    <what the user still has to check by eye: card appeared, went idle, click landed on the right window>
Skipped:       <anything not done, and why>
```

If something did not work, a requirement is not covered, or you needed a workaround, **ask the user** whether you should draft an email to `hi@vibeisland.app`. You write the draft and show it; the user reviews and sends it. Never send it yourself.

- It contains: what the agent is and does; the Vibe Island and macOS versions (`defaults read "/Applications/Vibe Island.app/Contents/Info" CFBundleShortVersionString`, `sw_vers -productVersion`); the terminal or app the agent runs in, and where a click landed if it was wrong; which events it sends and from where; the manifest without anything that looks like a secret; what you expected and what happened; the relevant `bridge.log` lines (`grep '"s":"acme-coder"' ~/Library/Logs/VibeIsland/bridge.log | tail -n 40`).
- It never contains: API keys, tokens or passwords; prompts, replies or other conversation content (including `prompt`, `last_assistant_message` and the text of `tool_input`); private file paths or project names the user has not agreed to share.

---

## Reference

Read a section here only when a step above links to it.

### Events

Event names use the Claude Code spelling. Other common styles are converted: `sessionStart`, `session_start`, `session-start` and `SESSION_START` are all `SessionStart`. A name that is not recognized passes through unchanged; use `eventMap` to translate it. `PermissionResult` is the exception: it is not converted, so send it exactly as written.

| Event | Useful fields | Effect on the card |
|---|---|---|
| `SessionStart` | `session_id`, `cwd` | Creates the card, idle. |
| `UserPromptSubmit` | `prompt` | Working; `prompt` becomes the summary. |
| `PreToolUse` | `tool_name`, `tool_input` | Working, with the tool name. |
| `PostToolUse` | `tool_name` | Working. |
| `PostToolUseFailure` | `tool_name` | Working. |
| `Stop` | `last_assistant_message` | Idle, with a completion notice when the reply is sent. |
| `StopFailure` | `last_assistant_message` | Idle. |
| `SessionEnd` | none | The card is removed. |
| `Notification` | `notification_type`, `tool_name`, `tool_input`, `message` | With `permission_prompt`, an approval reminder. Any other type returns the card to idle. |
| `PermissionResult` | none | Leaves the approval state; a tool event, `Stop` or the next prompt also clears it. |

### Fields

Send a JSON object on stdin with `snake_case` names. Other common styles are converted at the top level only (`sessionId` and `session-id` become `session_id`); keys inside `tool_input` are left alone. An unrecognized name passes through; use `fieldMap`. Names that start with an underscore are reserved: do not send them.

| Field | Type | Required | Notes |
|---|---|---|---|
| `hook_event_name` | string | Yes | The event name; without it the event is skipped. |
| `session_id` | string | Yes in practice | Identical on every event of a session. If missing, Vibe Island generates an unstable fallback id. See [Session ids](#session-ids). |
| `cwd` | string | Recommended | The card's project name; a hint passed to jump rules. |
| `prompt` | string | No | The user's message, for `UserPromptSubmit`. |
| `tool_name` | string | No | For tool events and permission reminders. |
| `tool_input` | object | No | Tool arguments such as `{"command":"ls"}`; a short summary shows on the card. |
| `last_assistant_message` | string | No | The reply, for `Stop` and `StopFailure`; long text is shortened. |
| `notification_type` | string | No | `permission_prompt` for approval reminders. |
| `message` | string | No | Free text for `Notification`. |

### Session lifecycle and cleanup

- `Stop` ends a turn; the next turn updates the same card. `SessionEnd` removes the card, and a late event for that id can bring it back. The command process exiting does not end the session.
- A card goes away only when `SessionEnd` arrives, the user removes it, a remote host's SSH connection drops, or idle cleanup removes it: a card idle (not working) for longer than the cleanup time, 2 hours by default, set in Settings > General > Idle session cleanup (30 minutes to 24 hours, or off).
- A session that shows as working but sends no event for 4 hours returns to waiting for input; the card stays. Send tool events during long tasks.
- A new session with the same source in the same terminal tab replaces the older card. After Vibe Island restarts, earlier custom sessions are not restored; one reappears with its next event.

### Approval reminders

If your agent has its own approval flow, tell Vibe Island that the user has to go back and approve, with the "Needs approval" command in [Step 4](#step-4-send-events). It is a reminder only: nothing is returned, and your agent decides in its own interface.

The card shows waiting for approval with the tool name and a short summary; a click takes the user back to your agent (see [Jumping back to your app](#jumping-back-to-your-app)). The next event your agent sends (a tool event, `PermissionResult`, `Stop`, `StopFailure`, `UserPromptSubmit` or `SessionEnd`) clears it. Send one reminder per approval, and do not send `PermissionRequest`; it is not part of this interface.

### stdout behavior

Every call prints one line, `{}` or your `stdoutResponse`, the same for every event and whether or not it was delivered. Empty or invalid stdin prints `{}` and sends nothing; if the app cannot be found nothing is printed. The exit code is 0 except for a missing `--source` (1). Delivery is one-way and best effort.

Ignore stdout. If your host puts hook stdout into a model's context or retries on some output, set `stdoutResponse` to something harmless, such as an empty string. If your code does read it, treat anything it does not explicitly recognize as "no decision": its content may grow in future versions.

### Session ids

You send `session_id`; Vibe Island prefixes it with your source name, so `{session_id}` in a jump rule is `<source>-<id>`: `demo-1` becomes `acme-coder-demo-1`, and an id that already starts with `acme-coder-` is not prefixed twice. `{raw_session_id}` is exactly what you sent (`demo-1`), on this Mac and for remote hosts; use it to find the session in your own app. Two ids that differ only by the prefix are the same session.

### Jumping back to your app

Verified on a real Mac with the build that ships this feature:

- A command-line agent in Terminal.app, iTerm2 or Ghostty: clicking its card lands on the window and tab the agent runs in. That includes a minimized iTerm2 window (restored and focused), several tabs in one iTerm2 or Ghostty window, and a tmux pane inside Ghostty.
- An agent that is its own macOS app, without a jump rule: clicking its card brings that app to the front.
- The same app with a jump rule in its manifest: the app is brought to the front and then receives the URL. `{raw_session_id}` is the id the developer sent and `{session_id}` is `<name>-<id>`.

Known issues in 1.0.53:

- Ghostty with several tabs running the same custom agent: a click can land on another tab of that agent when the session ids start with the same characters (`chat-1`, `chat-2`), or when the source name is longer than 12 characters. Random UUIDs as session ids and a source name of 12 characters or fewer avoid it.
- When a terminal tab is closed and the agent did not send `SessionEnd`, its card stays until idle cleanup or until the user removes it.
- A jump rule whose URL scheme no installed app handles makes macOS show a dialog saying no application is set to open the URL.

Not yet verified for custom agents: WezTerm, kitty, Warp, Electron-style apps where the agent runs in a helper process, and agents on SSH remotes. Ask the user to report what they see.

To land on the exact conversation inside your own app, add `bundleIdentifier` and `jumpRule` to the manifest and register a URL scheme in your app; Vibe Island opens it with the session hints filled in (variables and a handler example: [Custom Jump Rules](https://vibeisland.app/docs/custom-jump-rules/)). When you test a jump rule, note that macOS only routes a custom URL scheme to an app that is properly installed and signed. An unsigned build run from a temporary folder does not receive the URL even though Vibe Island sends it, so test with a normally built app in a normal location.

### Remote hosts (SSH)

Agents on a host connected through Vibe Island SSH Remote can appear in the same notch; this has not been verified for custom agents yet. The manifest keys `eventMap`, `fieldMap`, `terminatingTools` and `stdoutResponse` do not apply to them, so send standard names. Remote cards are removed when the SSH connection drops. For remote setup, email `hi@vibeisland.app`.

### What works today, planned, not supported

What is listed under Works today is what we support.

- **Works today:** live status, tool name and completion notice; your own name as a text tag or a first-letter badge; a click on the card returns to the terminal or app the agent runs in; read-only approval reminders; the optional manifest (display name, event and field mapping, automatic turn end for bots, jump rule).
- **Planned:** answer approvals and questions from the notch.
- **Not supported:** sub-sessions of an agent (no parent and child grouping); an agent's own usage or quota source; sending messages to the agent from the notch; a bypass button; automatic session naming; custom icons (the name shows as a text tag or a first-letter badge in a neutral color); restoring sessions after Vibe Island restarts; the Mac App Store edition.

### Compatibility

The event names, field names and manifest keys in this guide are stable. They are not removed or renamed and their meaning does not change. New abilities arrive as new events, fields or manifest keys, so an integration written against this guide keeps working.

---

Rendered page: https://vibeisland.app/docs/custom-agents-agent-guide/
