·
9 min read
Hook, slash command, skill, agent: who actually runs your AI?
Hook, slash command, skill, agent: who actually runs your AI?
If you use Claude Code, four words get thrown around as if they're interchangeable: *hook*, *slash command*, *skill*, *agent*. They aren't — and the difference isn't trivia. It decides what runs automatically versus only when you ask, who's in the loop, and — the part that bites people — the one credential step all four share, the one nothing reminds you to do.
There's a single question that sorts all four:
> **Who executes it — and does it run with *your* environment?**
To answer that, you first have to see something most tutorials skip: Claude Code isn't one thing. It's two — the app and the model — and which one runs your automation decides how it gets your keys, if at all.
1. The part nobody explains: the app is not the model
1. The part nobody explains: the app is not the model
When you type `claude` in your terminal, you start the **Claude Code app**. That app runs locally, on your machine, as you. It inherited your shell's environment when it launched — your `PATH`, your config, your API tokens. It manages the session, executes tools, fires hooks, and talks to Anthropic.
The **model** — the "me" you're chatting with — is not on your machine. It runs on Anthropic's servers. It has no hands. It can't open a file or run a command directly. What it can do is *decide* what to say and which tools to request. Every `Bash`, `Read`, or `Edit` you see it "do" is actually a request the app carries out on its behalf.
Here's the crucial bit — and it's the part that's easy to overstate. When the app runs a command *for the model*, that command still runs **locally**, and it inherits the environment Claude Code was launched in. So the model's `curl` *can* use `$FRESHJOTS_TOKEN` — **but only if you exported that token into your shell before you started `claude`.** If you didn't, or you set it after launch, or it only ever lived inside a hook's own environment, then the variable is simply absent and an authenticated call comes back `401`. That `401` is not a deliberate sandbox wall; it's plain environment inheritance — the model's shell has exactly what the launching shell had, no more, no less. The token reaches the model-run command only through a setup step *you* perform first, and one you can forget.
So the picture is: **app = a body; its pockets hold only what you put in before it left the house.** A *hook* is the body acting on its own reflex when an event fires — it carries whatever's in those pockets automatically, every time, no thought required at *fire* time. (You still packed them before leaving — the same step as everything else.) A slash command, skill, or agent runs from the same body and can reach the same pockets — but only for what you packed *before* `claude` launched. Skip that step and the model's hand closes on nothing: `401`.
*An actual Claude Code session: the app, the model, and a hook side by side — what each is and where it runs. One nuance the screenshot understates: the model's "no token" is the **no-setup default**, not an absolute — export the secret before launching `claude` and the model's shell inherits it too (exactly what the companion auto-archive and PR-description posts rely on). What's unconditional is only the *trigger* — a hook runs as the app, on its event, configured once and firing forever. Its reach to your token is just as conditional as the model's: the real archive hook aborts if you didn't export `FRESHJOTS_TOKEN` first.*
Once you have that two-layer picture, the four tools below stop being a list to memorize and become obvious.
2. The four things you'll actually configure
2. The four things you'll actually configure
**Hook.** A shell command *you* register in `settings.json`, tied to an event — a prompt submitted, a tool about to run, the session ending. The **app** runs it as its own child process the instant that event fires. Because it's the app's child, it inherits the app's environment — your `PATH`, and any secret you exported *before* launching `claude`. (The real Fresh Jots archive hook just aborts if `FRESHJOTS_TOKEN` isn't there — the same precondition every model-side tool has.) It is deterministic plumbing, not something the model chooses to do — the model often doesn't even see it happen. And you configure a hook *once*; it then fires on every occurrence of its event until you remove it. You never make "a hook per task."
**Slash command.** A saved prompt you trigger by typing `/name`. It's just text fed to the model, so the model does the work through its own tooling. That tooling runs locally and inherits the launch environment — so its `curl` carries your token *only if you exported it before starting Claude Code*. (Built-ins like `/clear` are handled by the app itself.)
**Skill.** A packaged playbook: instructions, optional scripts, and "use me when…" metadata. The model picks it up — automatically when a task matches, or because you invoked it — and runs it through that same model-side tooling, under the same pre-export precondition. If a slash command is shortcut *text*, a skill is a whole *procedure* the model can select on its own.
**Agent.** A separate, isolated Claude with its own context window, its own tool permissions, and its own brief. It works in its own room and hands back a summary — ideal for big searches or parallel work without cluttering the main thread. Its isolation is about *context*, not credentials: it runs on the same machine under the same launch-environment rule as any other model-side tool.
*The same four by what they are, who runs them, when, and how they get your token. Read the token column as **conditional for all four** — every one reaches it only when you've exported it before launch (the archive hook literally aborts otherwise). What's **unconditional** for the hook is the *trigger*: the app runs it as itself, on its event, every time. The durable split isn't "token vs no token" — they share that — it's "app, on an event, no per-use ritual" versus "the model, on demand."*
3. The comparison, in one breath
3. The comparison, in one breath
A **hook** is a shell command you register on an event; Claude Code itself runs it as the app, automatically, every time that event fires — it carries whatever you exported before launch — the very same export a slash command needs, no more. A **slash command** is a saved prompt you type as `/name`; the model runs it locally, on demand, and it reaches your token only if you exported it before launching `claude`. A **skill** is a packaged playbook the model runs the same way, under the same precondition — auto-selected or invoked. An **agent** is a separate, isolated Claude with its own context; same machine, same launch-environment rule, isolation that's about context rather than credentials. So the real split is not token versus no token: it's the hook running *as the app, on an event, with no per-use ritual*, versus the other three running *as the model, and only when you've wired the secret in beforehand*.
4. Why this matters: a real example
4. Why this matters: a real example
Fresh Jots auto-archives every Claude Code session into a note. The transcript is POSTed to the live API, authenticated with a real token, the instant you run `/clear`.
That works because it's a **hook** — actually one script registered on two events: `SessionEnd` (so it catches `/clear` and quitting) and `PreCompact` (so it also catches `/compact`, which never ends the session and so never fires `SessionEnd`). The app runs the hook script as its own child, so the script inherits `FRESHJOTS_TOKEN` from the shell you launched Claude Code in. The model is never in the loop — it doesn't even see it happen.
Could you build this as a **slash command** instead? You can — the companion PR-description and "tell your AI to write to a folder" posts do exactly that, and they work *because* they make you export `FRESHJOTS_TOKEN` before launching Claude Code, so the model's `curl` inherits it. What makes a hook the right tool *here* isn't a credential wall — it's the trigger. Those hooks fire on every `/clear`, `/compact`, and quit automatically, the model never in the loop. You still export the token once — same as the slash command — so the win here isn't the credential, it's the trigger. A slash command runs only when you type it, and authenticates only if you remembered the pre-export. That's exactly right for an on-demand PR write-up and exactly wrong for "archive every session, whether or not I think about it."
That's also why "can't you just make a hook every time I ask for a note?" is the wrong shape. A hook isn't an on-demand button — it fires on an *event*, not on "the user asked in chat." The right answer is *one* durable hook set up once (or, for occasional notes, a one-line authenticated `curl` you run yourself).
5. Rule of thumb
5. Rule of thumb
- Run with your credentials, deterministically, on an event? **Hook.**
- A reusable instruction you trigger by name? **Slash command.**
- A repeatable procedure the model can apply itself? **Skill.**
- A big or parallel sub-task done in isolation? **Agent.**
Three of the four are "the model doing things" at different sizes — one prompt, one procedure, one separate worker. Only the hook is the application acting *as you*. Get that distinction and Claude Code stops being a black box.
6. Going further
6. Going further
**Everything you can do here** — A good summary of your options.
**Get your Fresh Jots API token, then set it once** — The basic settings for starters.
**Get your Fresh Jots API token, then set it once** — The basic settings for starters.