Try it
In a terminal, with Gnista running:
G=/Applications/Gnista.app/Contents/Helpers/gnista
echo '{"session":"demo","event":"needs-you","message":"Ralph wants to deploy","cwd":"'"$PWD"'"}' | "$G" hook custom ralphA session from “Ralph” appears, asking for you. Click it and you’re back in this terminal. Then:
echo '{"session":"demo","event":"done","message":"Deployed"}' | "$G" hook custom ralph
echo '{"session":"demo","event":"end"}' | "$G" hook custom ralphThe helper always exits 0 and never waits, so it can’t hold your tool up. If something is wrong with what you sent, it says so on stderr and sends nothing.
The command
gnista hook custom <name> [event]<name> is your agent’s name: up to 32 lowercase letters, digits, - and _, starting with a letter or digit. Built-in agents’ names (claude, codex and the others) are taken. The event can be given here instead of in the JSON, for tools that run a different command for each event.
The JSON
One JSON object per call, on stdin:
| Key | What it is |
|---|---|
session | Required. Any id that stays the same for one session (text or a number). |
event | Required, here or on the command line. One of the events below. |
cwd | The project folder. Rows show it; without it, the helper’s own folder. |
message | What the agent is doing, asking or said last. |
tool | The tool in use, for tool and tool-done. |
title | The session’s title, shown instead of the folder. |
model | The model, e.g. gpt-5. Models Gnista knows show as a small badge on the icon; describe your own for the rest. |
transcript | The agent’s transcript file, for details (version 2). Claude-style hooks send it as transcript_path. |
pid | The agent’s process. When it exits, the session ends. Without it, the process that ran the helper. |
Events
Matched without regard to case or separators: needs-you, NeedsYou and needs_you are the same.
| Event | In Gnista |
|---|---|
start | The session appears, working. |
prompt | Working: someone asked it something. |
tool | Working, with “Running a command”, “Editing api.ts” or the message. |
tool-done | Still working, unless the tool is one of doneAfterTools. |
needs-you | Needs you: the notch says so, with the message. |
done | The turn is done. |
end | The session is over. |
A working session that goes quiet turns idle after a few minutes (Settings ▸ General ▸ Idle after).
The agent file
Optional. Put it at ~/Library/Application Support/Gnista/Agents/<name>.json. Every key is optional:
{
"name": "Ralph",
"icon": "ralph.svg",
"fields": {
"session": "$.conversation_id",
"event": "$.hook_event_name",
"cwd": "$.workspace_roots[0]",
"tool": "$.tool.name"
},
"events": {
"beforeSubmitPrompt": "prompt",
"afterToolUse": "tool-done",
"stop": "done",
"heartbeat": "ignore"
},
"host": "com.example.Ralph",
"jump": { "url": "ralph://open?session={session}" },
"doneAfterTools": ["attempt_completion"]
}| Key | What it does |
|---|---|
name | How rows and Settings name the agent, up to 40 characters. Without it, the name from the command. |
icon | An SVG or PNG file in the same folder, up to 1 MB. Without it, Gnista’s own spark. |
fields | For tools that send their own JSON: where to find each value from the JSON table above. Paths start at $, with .key for an object’s key and [0] for a list’s item. A field you don’t map is read from its usual key. |
events | Your tool’s event names, and which of Gnista’s each one is. "ignore" drops one quietly. Names match the same lenient way; Gnista’s own still work. |
host | The bundle id of the app the agent runs in, where a click takes you when Gnista can’t tell from the process. |
jump | A jump rule: see below. |
doneAfterTools | For agents that never say a turn is done: after any of these tools (a tool-done event), it is. |
A file over 1 MB, or with anything wrong in it (an unknown key, a path that isn’t one, an event Gnista doesn’t have), is ignored as a whole. Settings ▸ Agents ▸ Custom agents lists it with the reason, and the helper says so on stderr. The plain format above still works meanwhile.
Let Gnista connect it
With "version": 2, the file can also tell Gnista how to connect your agent, as it connects the built-in ones: which hooks to put in the agent’s own config, how to read what they send, whether its permission requests can be answered from the notch, and where its transcript is, so its row shows your prompt, its reply and the cost. Version 1’s keys keep their meaning.
{
"version": 2,
"name": "Ralph",
"colour": "#2EB872",
"adapter": "claude-style",
"hooks": {
"format": "claude-table",
"file": ".ralph/settings.json",
"events": ["UserPromptSubmit", "PreToolUse", "PostToolUse", "PermissionRequest", "Stop", "SessionEnd"],
"timeout": 5
},
"approvals": "plain",
"details": "claude"
}| Key | What it does |
|---|---|
version | 2. |
speaker | How a sentence names it: “Ralph wants to run…”. Up to 40 characters. Without it, the name. |
colour | Its colour, as "#RRGGBB". |
adapter | "fields" (the default): fields and events find Gnista’s values in your tool’s JSON, as in version 1. "claude-style": your tool sends Claude Code’s hook payloads; events then maps its own event names to Claude’s. |
toolNames | With claude-style: your tool’s names for Claude’s tools, like {"run_shell": "Bash"}, so rows and requests read the same. |
hooks | What Gnista installs when you connect it: the table below. |
approvals | Its permission requests wait for the notch: "plain" (allow or deny), "answers" (with answers to its questions) or "claude" (with answers, “Always” rules and a mode). Only with claude-style and a claude-table hook for PermissionRequest. |
details | Its transcript’s format, if it writes one Gnista reads: "claude", "codex", "gemini" or "antigravity". Rows then show its prompt, its reply and, but for antigravity, the cost. |
Settings ▸ Agents then lists your agent with the others: connecting it there adds Gnista’s hooks to the file you named, and disconnecting takes them out. Gnista changes only its own entries, and backs the file up first. The hooks call gnista hook custom <name>, with the event.
| In “hooks” | What it does |
|---|---|
format | "claude-table": Claude Code’s {"hooks": {"<event>": [{"hooks": [{"type": "command", …}]}]}}, the event in the payload. "flat": {"hooks": {"<event>": [{"command": …}]}}, the event on the command line. "toml": [[hooks]] blocks with event and command, in a file ending in .toml. |
file | The agent’s config, relative to your home folder and inside a folder of the agent’s own: .ralph/settings.json. That folder being there means the agent is on this Mac. |
events | The agent’s event names Gnista hooks: letters, digits and _. |
timeout | Seconds, 1 to 600, for agents whose hooks take one. |
foundBy | Files only the agent itself writes, relative to your home folder, when its folder alone doesn’t say it’s installed (other apps make it too). |
Jump rules
When you click a session, Gnista takes you to its terminal tab or window. For apps it has no way into, a jump rule gives it a URL to open instead, after the exact tab and window have been tried and before simply bringing the app forward.
{ "url": "myeditor://focus?folder={cwd}&pane={tmux_pane}" }Use it as "jump" in an agent file, or for every session in an app, as ~/Library/Application Support/Gnista/JumpRules/<bundle id>.json (for example com.example.Editor.json). An agent’s own rule comes first.
| Placeholder | Filled with |
|---|---|
{session} | The session id. |
{cwd} | The project folder. |
{tty} | The terminal, e.g. /dev/ttys004. |
{pid} | The agent’s process id. |
{tmux_pane} | The tmux pane, e.g. %3. |
Every value is percent-encoded as a whole (/ becomes %2F), so it can’t change the URL’s shape. A rule whose placeholders don’t all have a value isn’t used. URLs that open files or run code (file:, javascript:, data: and the like) are refused.
In Settings
Settings ▸ Agents ▸ Custom agents lists every agent with a file or that has reported, and when Gnista last heard from it.