# Hooks and what they can see

A Claude Code mod runs inside your session through hooks: it is called when a session starts, when Claude runs a tool, when part of the screen is drawn. This page lists every hook Adorably registers and every Claude Code API it calls, with what each one reads, what it ignores, and whether anything from it can leave your machine.

The list is not written by hand. It comes from `claude plugin validate`, which reads the mod’s code the way Claude Code will and reports what it hooks and calls. A script joins that report with the explanations below, and the build fails if the code hooks or calls anything this page does not explain. You can run the same check yourself on the [source zip](https://adorably.io/source): unzip it and run `claude plugin validate` on the folder.

The one hook that sees what Claude is doing is `tool.call`. For Bash it reads only the command text, to spot install commands such as `npm i` or `pip install`. Nothing from it is stored except package names, kept as local tags, and nothing from it is sent.

What the mod sends over the network is on the [home page](https://adorably.io/#sent) and in the [privacy policy](https://adorably.io/privacy).

Hooks module `./register.tsx`. Hooks that could block a tool call: `tool.call`, `ui.open`, `ui.close`. Adorably never blocks or changes one: it passes the call on unchanged, and a failure inside the mod lets the call through.

## Hooks

| Hook | When | What it reads | What it ignores | Can anything leave the machine? |
| --- | --- | --- | --- | --- |
| `session.start` | A session starts. | The session's folder (cwd), its surface (terminal, desktop, ...) and whether a person is at the prompt. Then, in the background: the mod's own store; if consent was never given (or was given under an older consent version) it asks; after a yes it fetches config and catalog and scans the project (see `$.fs.list` and `$.fs.read`). | Everything else about the session: the prompt, the transcript, the model, your account. | Only after you chose Turn on: GET `/v1/config` and GET `/v1/catalog` (no install id), and one queued session event (install id, how many offers matched). |
| `session.end` | A session ends. | Nothing from the event. It sends the queued events batch and stops its timers. | The whole event. | Only after you chose Turn on: the queued events batch (POST `/v1/events`). |
| `turn.start` | Claude starts working on your turn. | Only whether the turn belongs to the main conversation (agentId), to know Claude is busy: a card may show only then. | The prompt you typed and everything else in the event. | **No.** |
| `turn.complete` | Claude's turn ends. | Only agentId. Then the size and modification time of the manifests the last scan found (see `$.fs.stat`), to notice one changed by a command it does not parse; it rescans if so. It clears the card (after 20 seconds on screen) and closes a pane that peeked open. | The model's reply and everything else in the event. | **No.** |
| `tool.call` | Claude is about to run a tool. | The tool's name, and for two kinds of tool one field. Bash: only the command text, to spot install commands (npm i, pnpm add, pip install, cargo add, go get, ...); the package names found become local signals and then local tags. Write, Edit, MultiEdit: only file\_path, to see whether a manifest or config file inside the project is being written. After the tool runs it looks only at whether it was denied or failed, and rescans the project if it succeeded. | Every other tool, the file contents being written, the command's output, and every other field. It never denies or changes a tool call: it passes the call on unchanged, and a failure inside the mod falls through to the tool (validate: gating hook with .catch). It delays one only when a Write, Edit or MultiEdit starts while Adorably's pane has peeked open on its own: the peek closes first, and the tool waits until Claude Code has drawn the prompt at the width the peek gave back (at most 250 ms), so Claude Code's Diff pane can open for that change. | **No.** Nothing from the command is stored except package names as local tags, and nothing from it is sent. If the new tags make an offer match, only that offer's id can later be reported (shown, or eligible for sponsored offers). |
| `ui.open` | Any pane opens, the mod's own or another's (Claude Code's Diff pane, another mod's pane). | The pane's id and whether it was placed on screen. If another pane takes the space while Adorably's pane has peeked open on its own, the peek closes so it never crowds the other pane. | The pane's title and contents. It never blocks or changes another pane's opening. | Nothing. |
| `ui.close` | Any pane closes. | The pane's id, to forget that another pane is using the space. | Everything else about the pane. It never keeps another pane open. | Nothing. |
| `ui.render{component=PromptHint}` | Claude Code draws the hint line under the prompt. | Only the width it is drawn at on the terminal (the transcript's width, narrower while a pane is docked), so a file edit that closes a peek can wait until the prompt is drawn wide again (see tool.call). | The hint's text, whether you are typing, and every other surface. It draws nothing and passes the line on unchanged. | **No.** |
| `command.run{command=perks}` | You run `/perks` (with or without arguments). | Only the arguments of `/perks` (off, on, why, privacy, settings ..., forget). | Every other command. | Only what the subcommand does: `/perks forget` sends POST `/v1/forget` with the install id. The command's text answer goes into the transcript, where the model can read it, so it never includes the install id. |
| `ui.render{component=AbovePrompt}` | Claude Code draws the band above the prompt. | The band's width, whether the terminal is fullscreen, and whether a survey is showing (a card never covers it). Draws the moment card when there is one. | The prompt text and the rest of the screen. | **No.** Pressing a card's buttons queues events (shown, dismissed, muted, claimed) by offer id. |
| `ui.render{component=SessionMode}` | Claude Code draws the session modes at the right of the prompt footer. | The list of modes already there, to add one dim label: "◆ N perks", only while something matches. | Claude Code's own notices; it never removes or changes another label. | **No.** |
| `ui.render{component=Pane, requestId=adorably}` | Claude Code draws Adorably's own Perks pane. | The pane's width. Everything it shows comes from the mod's own state. | Every other pane. | **No.** Its buttons do what the matching /perks commands do (a claim sends POST /v1/claims). |

## Claude Code API calls

| Call | What it reads or does | What it ignores | Can anything leave the machine? |
| --- | --- | --- | --- |
| `$.clock.after` | Nothing. One-shot timers: the 5-second network timeout, keeping a card up for at least 20 seconds, and the most a file edit waits after a peek closed (250 ms). | — | **No.** |
| `$.clock.every` | Nothing. Repeating timers: the 10-minute tick (config, events, catalog), a 5-second look in the mod's own store for mutes, dismissals and claims made in another session, and Ado's wing flaps. | — | **No.** |
| `$.clock.now` | The current time, for frequency caps, cooldowns and event timestamps. | — | Event timestamps (from your computer's clock) go in events batches. |
| `$.command.register` | Nothing. Registers the `/perks` command. | — | **No.** |
| `$.env.get` | Four variables: `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC`, ADORABLY\_API\_BASE (a demo or staging api), and, only to update itself, HOME and CLAUDE\_CONFIG\_DIR (to find the claude binary and how Claude Code installed this copy). | Every other environment variable. | **No.** |
| `$.fs.list` | File and folder names in the project, at most three folders deep, skipping node\_modules, .git, dist, build, .venv, venv, vendor, target, .next, .turbo, \_\_pycache\_\_, coverage and .claude-plugin. | Everything outside the project folder. | **No.** Names are matched against the catalog's dictionary locally. |
| `$.fs.read` | Dependency manifests (package.json, requirements\*.txt, pyproject.toml, go.mod, Cargo.toml, Gemfile; at most 16), env sample files (.env.example, .env.sample, .env.template, .env.dist, example.env), from which only the variable NAMES are kept, and prisma/schema.prisma, from which only the datasource block's provider is kept. To update itself, installed\_plugins.json under the Claude config folder (only plugin ids and install paths), when claude plugin list --json cannot run. | Source files, .env, .env.local and every other file. Values in env sample files. Everything in schema.prisma but the datasource provider. | **No.** |
| `$.fs.stat` | Size and modification time of the manifests and env sample files the last scan found (at most 24) and of the project folder. | File contents. | **No.** |
| `$.http.fetch` | Responses from the api base only (https://api.adorably.io, or the demo or staging base ADORABLY\_API\_BASE names): config, catalog, claim answers. | Every other host: the mod fetches nothing else. | Yes, after you chose Turn on, and never under `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` or `/perks off`: GET `/v1/config`, GET `/v1/catalog`, POST `/v1/events` (install id, offer ids and what happened, per-session match count, surface, mod version, Claude Code version, batch id, event times), POST `/v1/claims` (install id, offer id, surface, card or pane), POST `/v1/forget` (install id). Every request carries `x-adorably-mod`: <version> and User-Agent: adorably/<version>. |
| `$.process.run` | Runs these commands only: uname once (to pick the opener), then open (macOS) or xdg-open (Linux) with an https:// link when you claim or open a perk; and, to update itself, claude plugin list --json (to find how Claude Code installed this copy), then at most once a day claude plugin marketplace update <marketplace> and claude plugin update adorably@<marketplace>. The claude binary is the one on PATH, else ~/.local/bin/claude. | Nothing else is ever run. The update commands never run in a headless session, with updates manual, with the network off, after `/perks off` or while the kill switch is on. | Your browser opens the link: Adorably's tracked claim link (api.adorably.io/r/...) or, when the API is unreachable, the vendor's page with ref=adorably. An update makes Claude Code fetch the marketplace file and the release zip from adorably.io, as a manual update does, with no install id. |
| `$.session.cwd` | The session's folder, after a hot reload (session.start gives it otherwise). | — | **No.** A local hash of it keys per-project memory; neither is sent. |
| `$.session.surface` | Which surface draws the session (terminal, desktop, vscode, mobile), after a hot reload. | — | The surface name goes in events and claims. |
| `$.session.version` | Claude Code's version number. | — | Yes: as host\_version in each events batch (since 0.1.1), so a break in the mods API shows up per Claude Code version. |
| `$.state.get` | The mod's own in-memory view (adorably.view, card, tab, cardWhy, frame, expanded), while drawing. | Every other plugin's state. | **No.** |
| `$.state.set` | Writes the mod's own in-memory view, so the card, pane and footer redraw. | — | **No.** |
| `$.store.delete` | Deletes a sent events batch, and every key on `/perks forget`. | — | **No.** |
| `$.store.get` | The mod's own store on this machine: consent, install id, settings, cached catalog and config, queued events, frequency counters, mutes, dismissals, claimed perks, removed tags, per-project memory. | Other plugins' stores. | Only what events, claims and forget send (see `$.http.fetch`). |
| `$.store.keys` | The names of the mod's own keys, to delete them all on `/perks forget`. | — | **No.** |
| `$.store.set` | Writes the keys listed under `$.store.get`, on this machine. | — | **No.** |
| `$.ui.ask` | Your answer to the first-run question (Turn on or Not now). Anything else counts as no answer. | — | **No.** |
| `$.ui.close` | Closes a pane that peeked open, when the turn ends or the mod goes quiet. | — | **No.** |
| `$.ui.copy` | Copies a perk's code, or its link when nothing can open it, to your clipboard. | — | **No.** |
| `$.ui.focus` | Puts the keyboard focus back on the pane tab you chose. | — | **No.** |
| `$.ui.log` | Writes failures, batch outcomes, a rejected ADORABLY\_API\_BASE and update attempts (including this plugin's own folder) to Claude Code's debug log only. | — | **No.** |
| `$.ui.open` | Opens the Perks pane: when you run `/perks`, or once a session as a peek for a strong match (144+ columns, fullscreen, peeking on). | — | **No.** |
| `$.ui.resolve` | The surface's drawing elements (Box, Text, Button) for the card and the pane. | — | **No.** |
| `$.ui.toast` | Shows a short notice ("Code copied · Opening ..."). | — | **No.** |

## Environment variables read

| Variable | What it reads | Can anything leave the machine? |
| --- | --- | --- |
| `ADORABLY_API_BASE` | Whether it is set, and its value: the api to use instead of https://api.adorably.io, for demos and staging only. Only an https:// URL, or http:// on 127.0.0.1 or localhost, is accepted; anything else is ignored with a debug log line. | Only where requests go: when set and accepted, every request the mod makes (and the check that a claim link is the api's own) uses that base. The value itself is not sent. |
| `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` | Whether it is set (anything but empty, 0 or false). | **No.** When set, the mod makes no network request at all, /perks forget included. |
| `CLAUDE_CONFIG_DIR` | Only to update itself: where installed\_plugins.json is (default ~/.claude), when claude plugin list --json cannot run. | **No.** |
| `HOME` | Only to update itself: to find ~/.local/bin/claude when claude is not on PATH, and ~/.claude when CLAUDE\_CONFIG\_DIR is not set. | **No.** |

The module writes no environment variables. Its `$.state` keys are its own (`adorably.*`) and live in memory only.
