Custom agents,
one line of JSON.

Any tool can show up in Gnista: working, done, or needing you. No release of Gnista needed, and nothing to install beyond Gnista itself. Each time something happens, your tool pipes one line of JSON to Gnista’s helper.

Updated

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 ralph

A 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 ralph

The 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:

KeyWhat it is
sessionRequired. Any id that stays the same for one session (text or a number).
eventRequired, here or on the command line. One of the events below.
cwdThe project folder. Rows show it; without it, the helper’s own folder.
messageWhat the agent is doing, asking or said last.
toolThe tool in use, for tool and tool-done.
titleThe session’s title, shown instead of the folder.
modelThe model, e.g. gpt-5. Models Gnista knows show as a small badge on the icon; describe your own for the rest.
transcriptThe agent’s transcript file, for details (version 2). Claude-style hooks send it as transcript_path.
pidThe 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.

EventIn Gnista
startThe session appears, working.
promptWorking: someone asked it something.
toolWorking, with “Running a command”, “Editing api.ts” or the message.
tool-doneStill working, unless the tool is one of doneAfterTools.
needs-youNeeds you: the notch says so, with the message.
doneThe turn is done.
endThe 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"]
}
KeyWhat it does
nameHow rows and Settings name the agent, up to 40 characters. Without it, the name from the command.
iconAn SVG or PNG file in the same folder, up to 1 MB. Without it, Gnista’s own spark.
fieldsFor 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.
eventsYour 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.
hostThe bundle id of the app the agent runs in, where a click takes you when Gnista can’t tell from the process.
jumpA jump rule: see below.
doneAfterToolsFor 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"
}
KeyWhat it does
version2.
speakerHow a sentence names it: “Ralph wants to run…”. Up to 40 characters. Without it, the name.
colourIts 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.
toolNamesWith claude-style: your tool’s names for Claude’s tools, like {"run_shell": "Bash"}, so rows and requests read the same.
hooksWhat Gnista installs when you connect it: the table below.
approvalsIts 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.
detailsIts 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.
fileThe 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.
eventsThe agent’s event names Gnista hooks: letters, digits and _.
timeoutSeconds, 1 to 600, for agents whose hooks take one.
foundByFiles 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.

PlaceholderFilled 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.