Hooks · crystl Docs

Approval cards, turn-end status, and Vigil all depend on one mechanism: agent hooks. Claude Code fires a hook at each event in a session (a tool request, a turn ending, a question), and crystl listens. This page covers exactly what crystl installs to make that work, how the install behaves, and how to remove it.

If a panel never appears, or crystl shards shows a flat idle for an agent you know finished, the hook wiring is the first thing to check: run crystl doctor. See troubleshooting for the common failures.

What gets installed

Three things, all under ~/.claude:

crystl installs these the first time you use Claude Code in a gem, and re-checks them after that. There is nothing to set up yourself.

The events, and what each one powers

crystl registers ten events:

Event What crystl uses it for
PermissionRequest The approval card: the request appears on screen, and your answer flows back as the decision
PreToolUse Enforcement: your auto-approval mode answers here without asking
PostToolUse Closes Vigil’s in-flight state for the tool call
Stop Turn-end status: ✓ declared done vs ❓ needs you in crystl shards, crystl wait done, and the push to your phone
StopFailure A turn that ended in an error is reported as one
UserPromptSubmit Marks the agent as working again
SubagentStop Subagent completion, for activity tracking
Notification Agent notifications surface as glass panels
PermissionDenied Denials are reported, so a parked agent is flagged instead of looking busy
SessionStart Ties a new agent session to its shard

This is why the features fail together: no hooks means no panels, no turn-end status, and no Vigil, all at once.

Jev: optional help reading a turn ending

The Stop row above is decided on your Mac by reading the agent’s closing message. It handles the clear cases well. It misses some of the ambiguous ones, like an agent that asks you something in the middle of a long summary and then stops.

Jev is an optional second opinion on that one decision, for Guild members. It is off by default for everybody. The switch is in settings → agents → defaults. While it is off, nothing is sent anywhere and the programmatic answer is the one used.

Turn it on and the closing message of each finished turn is classified into one of three shapes: the agent declared done, it is waiting on you, or it asked a direct question. In testing this caught roughly 30% more of the moments where an agent was actually waiting on a person.

What leaves your Mac, and what does not:

The programmatic answer is the fallback, never a guess. A non-200, a malformed body, an answer below the confidence floor, or anything slower than one second, and the shape your Mac worked out is the one you see. The proxy allows 2,000 classified turns per licence per day and falls back the same way once you pass it.

Jev can move the answer in either direction among those three shapes, so it is a different read rather than a strictly safer one. That is the trade for catching the ambiguous endings, and it is why the feature is a switch rather than the default.

Hooks only act inside crystl

The first thing crystl-hook.sh does is check for CRYSTL_SHARD, the environment variable every crystl shard carries. Outside a crystl shard, the script answers Claude Code with an empty {} and exits immediately. Running Claude Code in Terminal.app or another terminal costs one no-op script call per event and changes nothing about the session.

The script also exits instantly when the crystl app has never run or is gone: no bridge token, no bridge socket, no SSH configuration means there is nothing to talk to, so it answers {} without probing any port. Hook entries left behind by an uninstall cost nothing per event.

Installs are surgical and re-runnable

settings.json is yours; crystl treats it that way.

Removing the hooks

Dragging Crystl.app to the trash runs no code, so it removes neither the entries nor the script — and the crystl command is a symlink into the app bundle, so it breaks with the app. Run the uninstall before you trash the app. The easiest way is the uninstall crystl… button in settings (general → terminal): it runs the same cleanup, says what it removed, and then offers to move the app to the trash for you. The terminal way, while the command still works:

crystl uninstall

It prints everything it would change, asks once, then applies. Free on every tier, and it works with crystl not running. --dry-run prints the plan and stops; -y skips the confirmation.

Already trashed the app? Reinstall it (or drag it back out of the trash) and run the uninstall from there, or let crystl doctor name what was left behind.

What it undoes:

Two things it deliberately does not touch. Your work — per-gem .crystl/ state, crystl/* branches and worktrees, saved API keys in the Keychain — is never deleted; the command lists where each one is stored so you can decide. And the CLI primer sections in your projects’ agent docs are edits to files in your git repos, so they come out only when you ask with --primers (crystl must be running so it can list your gems).

Running it twice is fine: a second run finds nothing installed and says so. Every flag is in the CLI reference.

crystl doctor checks the wiring’s health either way: entries without a script, a script without an app, or a leftover stub each get named plainly, with the fix offered. See troubleshooting.

Codex

Codex supports hooks too, wired through ~/.codex/hooks.json rather than Claude’s settings file. Codex asks you to trust the hook the first time it sees it; that dialog and why crystl never auto-approves it are covered in the FAQ. crystl doctor checks the Codex file as well when Codex hooks are enabled.

Related docs