# Hooks

> What crystl installs into ~/.claude, why approval panels and turn-end status depend on it, how installs stay surgical, and how to remove it all.

Approval panels, 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](/docs/troubleshooting/) for the common failures.

## What gets installed

Three things, all under `~/.claude`:

- **Hook entries in `~/.claude/settings.json`.** One entry per event crystl listens to, each running the same command: `bash ~/.claude/crystl-hook.sh <event>`. The tool-scoped events carry a `*` matcher, so every tool is covered.
- **`~/.claude/crystl-hook.sh`**, the script those entries invoke. It forwards each event to the crystl app over the local bridge and returns the app's answer to Claude Code.
- **`~/.claude/crystl-augment.py`**, a helper the script runs on the turn-end (`Stop`) event to enrich the payload before forwarding it.

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 panel](/docs/approval-modes/): the request floats on screen, and your answer flows back as the decision |
| `PreToolUse` | Enforcement: your [auto-approval mode](/docs/approval-modes/) answers here without asking |
| `PostToolUse` | Closes [Vigil](/docs/vigil/)'s in-flight state for the tool call |
| `Stop` | Turn-end status: `✓ declared done` vs `❓ asked a question` 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](/docs/notifications/) |
| `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.

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

- **Only its own entries are touched.** crystl identifies its entries by the command they run, replaces those, and leaves every other hook (yours, or another tool's) exactly as written.
- **It validates before it writes.** The installer checks the structure of the entire hooks map first. Anything malformed, or a `settings.json` that is not valid JSON, means crystl refuses to write at all rather than risk normalizing something it does not own.
- **Writes are atomic.** The new content lands in a temporary file that replaces the old one in a single step, preserving the file's permissions. A concurrent write can never leave a half-written `settings.json`.
- **Re-running is free.** The installer is idempotent: if the installed state already matches, nothing is written. Every new shard re-runs it, which is also how an update rolls out a newer hook script.

## 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 dies 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:

```bash
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`](/docs/troubleshooting/#hook-failures) name what was left behind.

What it undoes:

- **The hook entries come out of `settings.json`.** Removal uses the same detector as install, so exactly crystl's entries go and every hook you added survives verbatim. The file itself is never deleted; it holds your own configuration.
- **`crystl-hook.sh` becomes a no-op stub, not a deleted file.** A Claude Code session that is already running loaded `settings.json` at startup and keeps calling that path for the rest of its life. Deleting the file would make every one of those calls fail with exit 127 until the session restarts, so a stub that answers instantly stays behind. The stub says so in its own comment and is safe to delete once your sessions have restarted.
- **The support files are deleted**: `crystl-augment.py`, the SSH bridge config, and the shell-integration files under `~/.claude`.
- **Codex and Antigravity CLI hook wiring** is removed the same way, entries out and scripts stubbed, with your own hooks kept.
- **The CLI symlink and bridge token go too.** The symlink is deleted only after crystl proves it points into its own app bundle; a `crystl` you installed yourself is reported and left alone.

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 lives 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](/docs/cli/#uninstall).

`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](/docs/troubleshooting/#hook-failures).

## 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](/docs/faq/#codex-says-hooks-need-review-what-do-i-do). `crystl doctor` checks the Codex file as well when Codex hooks are enabled.

## Related docs

- [approval modes](/docs/approval-modes/): what answers a `PreToolUse` event without asking you
- [notifications](/docs/notifications/): the panels the events feed
- [vigil](/docs/vigil/): the watcher that reads tool-call and turn-end events
- [use Claude Code with crystl](/docs/agent-claude-code/): the full integration list
- [troubleshooting](/docs/troubleshooting/): when the wiring breaks

---
Source: https://crystl.dev/docs/hooks/
