The command every kit plugin's verbs mount into, and the tooling around a session. It knows no plugin by
name: each one declares a cli block in its plugin.json — {command, module | entry, attr, hidden?, hoist?},
one entry or a list — and ak mounts it at start (src/ak/cli.py: _mount_plugin_clis) when the plugin is
present AND enabled in Claude Code (enabledPlugins, user · project · local settings). The id read is the one Claude
Code has for that folder, whatever the marketplace (plugin_inspect/installed.py: ids_for — an installed_plugins.json
installPath that is the folder, else the marketplace in known_marketplaces.json whose clone holds it): tracer@ak from
the release clone, tracer@agentic-kit in the public kit. A folder nothing on disk names (a worktree) reads any
tracer@…, on when any is true. When enabledPlugins cannot be read — no settings naming a plugin,
$AK_PLUGINS_DIR, pytest — present is mounted. A plugin
switched off leaves no verb, no root Examples line and no COMMAND_GROUPS entry; its extras (ws move on the
vault's workspaces) go with it. A module may also expose brain_status() (a row on the bare-ak band),
brain_boot(ctx) (runs before any verb) and brain_search(words, limit) (its rows in ak search).
~/.ak is ak's home (src/ak/home.py): before any verb, and before any plugin's brain_boot, ak plants
the layout-2 skeleton — vault/ db/ cache/ moves/ and the layout stamp — from the layout move/plan.py
declares. Never over anything: a folder is a mkdir, the stamp is written only when absent, and a home that
holds something but no stamp (layout 1) is left as it is for scripts/migrate/4-home-layout.py. The vault's
boot then plants only its notes into ~/.ak/vault; with no vault plugin, ak doctor passes on the skeleton.
ak search (s) every plugin that can search, at once (each one's brain_search)
ak shim · gateway · xray · sysprompt the `claude` you launch, the model-call wire, the prompt it sends
ak hooks on|off|show hook tracing
ak ws move · moves · doctor · home moving a workspace and everything that points at it (src/ak/move/)
ak plugin ls · inspect · log · rename what your plugins put in front of your agent (src/ak/plugin_inspect/)
ak sessions … · trace … · memory … … mounted: each plugin's own verbs
ak -h groups the root in four sections — Everyday, Groups, Setup & state, Plumbing (help.py: COMMAND_GROUPS).
src/ak/groups.py holds what every group shares: FuzzyGroup (exact name → alias → unique prefix → a verb
typed one level too high, run from where it lives when it only reads: ak replay x prints → ak sessions replay x), writes (a verb that changes something: ✎ in every list, never run on a guess) and hidden_alias (an old
spelling that keeps working, out of help and suggestions).
ak setup is the kit's one installer. It looks at the machine, lists the components it can install (the ones it can't are greyed out with the reason), previews every settings and shell-rc change as a diff, applies in dependency order, then checks the result.
A fresh machine has no ak yet, so it starts from the public kit, proxify-dev/agentic-kit:
uvx --from "git+https://github.com/proxify-dev/agentic-kit#subdirectory=plugins/ak" ak setup
At a terminal it opens a picker with five screens, numbered 1/5 to 5/5: check (what is on this machine, held until enter: only the tools something in this kit runs on, and the size of your Claude Code history; a missing uv, git or claude is named with where to get it), choose (one row per plugin with its one-line setup.summary, under a heading that says where it acts: inside Claude Code, or around the claude you type; setup's own clone, marketplace, the ak command and the ak plugin are one ak line, always installed, never a row to untick; under the list, the row under the cursor: picked or left out, what you get (setup.gives), its first run with the size of your history, what it puts on your machine (setup.changes, else "nothing outside Claude Code") and what it brings along; an installed plugin is a ✓, not a box; space ticks, enter next, d the defaults; unticking something others need asks for a second space), confirm ("Ready to install": you get (each pick with its summary), then (a first run), on your machine (each pick's changes, and a settings.json diff), asks you to (a browser sign-in), left out (each row not picked, and why when it can't run here) — then enter to install; an Enter within half a second of arriving, the one that left choose, is ignored; d shows each step with its command or settings diff, b goes back), install (every step with its state beside the output of the one running; a step that needs the person, a sign-in, gets the terminal under a heading that says what it is and that setup comes back; a step that fails offers r retry, s skip, u undo; while a plugin's first run goes, its row shows its own count and a clock, and enter finishes setup and leaves it going) and done (the checks, setup's own and the ak plugin as one ak line while they pass; a pick that passes shows its first gives line, what you can do with it now; one a new terminal picks up (the shim, ak on PATH) is a ·, not a warning; each first run: done, or still going; r goes back to confirm with what still fails, enter finishes, and the outcome, each pick's first gives line and what to run next stay in the terminal). Anywhere else (a pipe, an agent, --dry-run, --json, --only) it prints the plan and exits 0. Only --yes writes (looking at your claude can make Claude Code touch its own files, but setup does not).
ak setup --dry-run the plan and every diff; setup itself writes nothing
ak setup --yes the defaults, without asking
ak setup --yes --only memory just memory, and what it needs (it says what it pulled in)
ak setup --json the same data as one JSON document
ak setup --repo URL the kit from URL (else $AK_REPO_URL, the repo uvx built from, the clone's origin)
ak setup status what is installed, and the checks on it (exit 1 when one fails)
ak setup undo take the newest run back
Where the kit comes from decides who keeps its clone.
| the kit is | claude plugin marketplace add gets |
the kit's clone (the "kit dir") | updates |
|---|---|---|---|
a URL Claude Code clones: https://github.com/o/r (also git+https://…, git@github.com:o/r), any other https://, http:// or ssh URL |
o/r for GitHub, else the URL |
Claude Code's own, <claude home>/plugins/marketplaces/<name>/; setup clones nothing |
Claude Code's: setup sets extraKnownMarketplaces.<name>.autoUpdate: true in settings.json |
a path, or a file:// URL (marketplace add refuses file://) |
the release clone's path | the release clone, git cloned by setup to <data dir>/ak/release |
none: Claude Code never refreshes a local folder; RELEASING.md |
The public kit, and the uvx line above, take the first row: marketplace add proxify-dev/agentic-kit, then a settings step, previewed as a one-key diff, that adds "autoUpdate": true to the extraKnownMarketplaces.agentic-kit entry marketplace add wrote (it rewrites that entry without the key on every add, so a second ak setup puts it back; the step never makes the entry itself, and is skipped when it is not there). With it, Claude Code refreshes the marketplace in a session and updates a plugin when its plugin.json version changes; the plugins run from Claude Code's versioned copies, <claude home>/plugins/cache/<name>/<plugin>/<version>/. The ak command is installed editable from <kit dir>/plugins/ak, a path that holds across updates (Claude Code swaps the whole folder). The tracer's environment is built, and its first run started, in its cached copy (the installPath that installed_plugins.json holds once it is installed, read when the step runs): never in Claude Code's clone, which an update replaces. Undo runs claude plugin marketplace remove <name>, which also uninstalls every plugin from it, deletes their data folders (plugins/data/<plugin>-<name>) and empties its extraKnownMarketplaces and enabledPlugins entries; the tracer's index (plugins/data/conversation-index) and the cached copies stay on disk.
Which row a run takes: --repo or $AK_REPO_URL, else a marketplace Claude Code already cloned (so ak setup status needs nothing passed), else what uvx built from, the release clone's origin, ~/agentic-kit. This workspace's kit has no remote and stays on the second row. A machine set up before this change, whose agentic-kit marketplace is the release clone, is refused, not moved: the refusal names claude plugin marketplace remove agentic-kit (it uninstalls the kit's plugins) and ak setup --repo <url>.
The list is read from the kit on disk, never kept in setup: the checkout ak runs from (this workspace, or the release clone), else the release clone. Every plugin under plugins/ is a component (its plugin.json: name, description, visibility, dependencies, and an optional setup block for what the rest can't say: summary, gives (what you get, one line each, led by the command that tries it), changes (what it puts on the machine outside Claude Code's plugin folder), route (a piece that changes how claude starts: the path it takes once that piece is picked), needs, default, note, depends_on, first_run; src/ak/setup/catalog.py has the shape), and every module in kit/modules.json (the kit's shim/ and gateway/ folders). Here every plugin is listed and the private ones say so; in the public kit (proxify-dev/agentic-kit) only the public ones exist. An ak with no kit on disk yet (the uvx line above, before the clone) reads src/ak/setup/kit.json, the public kit as scripts/kit.py render wrote it.
| component | what it is | needs |
|---|---|---|
release |
the kit's clone, every plugin installs from it: Claude Code's for a kit at a URL (made by the marketplace step, which asking for it brings), else the release clone at <data dir>/ak/release |
git |
marketplace |
the kit's marketplace (agentic-kit, ak here): a clone of the kit's URL with auto-update on, or the release clone |
claude; release |
ak-tool |
the ak command, as one uv tool (what hooks/install-global.py installs), editable from the kit's clone |
uv; release |
ak-path |
uv's tool folder on your shell PATH; skipped when it already is | ak-tool |
ak vault memory |
the host (with ak plugin), the link engine, memory |
each on the one before it |
observer tracer |
what past sessions learned; which session did what. Each builds its venv now; the tracer then indexes your history (its first run) | uv; ak |
tool-results tag |
big results on disk; session tags | marketplace, vault |
notify |
a macOS popup when a session is done | macOS, osascript |
agentic-engineering |
seven skills (context, harness, skill and agent development, evals, sandboxing, fewer faster tests), most named by a hook at its moment, an agentic-engineer agent, /new-plugin, and /new-agent and /new-skill (they need settings.function-hooks, which comes with it) |
marketplace |
feed |
off by default | marketplace |
settings.function-hooks |
env.CLAUDE_CODE_ENABLE_FUNCTION_HOOKS in settings.json: the commands of agentic-engineering run on it. Comes and goes with that plugin, no row of its own |
|
settings.file-suggestion |
fileSuggestion in settings.json: @ completes vault notes |
vault |
shim |
the claude wrapper: cc-shim.mjs install from the kit's shim/, which copies itself into its own folder; comes with the gateway, no row of its own (--only shim alone) |
macOS/Linux, node ≥ 22.13, claude; the kit's clone |
gateway |
the model-call wire as a service, run from the kit's gateway/: ak gateway account login (a browser, when it has no account), then ak gateway enable, which copies gateway/src into the gateway's own folder before the service starts (off by default) |
macOS/Linux, node ≥ 22.7, a service manager; shim, the kit's clone |
The claude you type: the gateway, and the shim that comes with it. Choose shows one row, gateway, off by default: it changes how claude starts, so a person picks it. The shim comes with it and has no row of its own (setup.comes_with): it is how the claude you type reaches the gateway, and what reads claude --account NAME. The gateway's row says so (comes with claude shim), lists the shim's perks among its own, and shows how claude starts, marked with the one the picks give; confirm says it again (claude starts):
| you pick | the claude you type |
you get |
|---|---|---|
| nothing here | claude → Claude Code → Anthropic, on Claude Code's own login | Claude Code as it is |
gateway (the shim with it) |
claude → shim → Claude Code → gateway → Anthropic, on your accounts in turn | your Claude accounts in one pool, the next one answering when one runs out; claude --account NAME starts on one; every request recorded (ak gateway, ak xray); your own instructions on every start (ak sysprompt) and your own scripts before claude starts (the shim's conf.d). You sign in once, in a browser (or from a link you paste back, over ssh or in a container); Claude Code's own login is not used, and its banner says "API Usage Billing" |
The shim alone (your instructions and scripts, Claude Code's own login, and claude --account refusing: there is no account list to pick from) is ak setup --yes --only shim. A gateway with no shim reaches only cc-gateway run -- claude; setup does not offer it. ak gateway disable gives claude its own login back while the shim stays.
Nothing is downloaded for either: both run from the kit's clone, and undo runs cc-shim uninstall and ak gateway disable. A run from ak 4.4 or older recorded its undo as the module's install.sh --uninstall fetched from GitHub; undo now does what that script did, on this machine. tasks, routines and todo are listed but not selectable: they are not in the marketplace.
A plugin's first run. setup.first_run ({"title", "run", "reads"}) is the plugin's own work once it is installed: the tracer's indexes your Claude Code history (bin/tracer trace index --all). It is the last step, started in its own session from the plugin's folder (its cached copy for a kit at a URL, else the release clone), so it keeps going when the person finishes setup before it ends (the TUI's enter, or Ctrl-C at the plain door); ak's tracer row says indexing your history · 12/26 projects meanwhile. "reads": "history" puts the size of <claude home>/projects beside the title; with no history there is no step, and the plan says why.
What it leaves alone. A component already in place makes no step, so a second run is a repair. It never pulls an existing clone and never re-points the kit's marketplace: a marketplace that points elsewhere (another folder, another repo, or a local folder where the kit is now a URL) is a refusal that names the command to remove it. An autoUpdate somebody set to false stays (status warns). A settings.json key that already holds another value stays as it is. The observer's embed is not a step (minutes of work): run ak observer store embed later.
The run folder. Each --yes run is <kit home>/db/setup/<YYYYmmdd-HHMMSS>/: run.json (the steps and how to take each back), backup/ (every file a step was about to write, as it was) and logs/ (each step's output). ak setup undo [RUN] walks the newest run back newest step first, and takes back only what that run did: settings keys whose value is still ours, rc lines it added, a file nobody wrote since, the release clone when it is clean, what it installed (the marketplace it added, with what Claude Code removes along with it, said in the undo's detail). It runs while Claude Code sessions are open, and a second undo does nothing. It leaves the venv builds (a cache) and the gateway account (cc-gateway account remove <name>). --list lists the runs, --dry-run shows what it would do.
Tested with fake claude, uv, git and node on PATH, and run end to end from the uvx line on a fresh Debian box with systemd and ~100 MB of real history (every public piece, the gateway's sign-in, then a claude call through the shim and the gateway). Not run on Windows, nor on a fresh macOS user. The URL row's marketplace steps (add from GitHub, auto-update, status, a second marketplace add and the repair, undo) were run against the real claude in a throwaway HOME on macOS; the whole URL flow on a fresh box has not run yet.
hooks/install-global.py (SessionStart) puts ak on PATH as one uv tool, editable from the release clone:
ak with the vault and memory packages installed beside it (--with-editable ../vault ../memory,
each when it is on disk). From a GitHub marketplace, where Claude Code runs ak from its versioned cache
(<claude home>/plugins/cache/<marketplace>/ak/<version>, a new path each version, deleted 14 days later), it installs
from Claude Code's clone of that marketplace instead (<claude home>/plugins/marketplaces/<marketplace>/plugins/ak),
one path across versions. With the clone missing it installs from the cache path only when no ak is installed
yet; otherwise it changes nothing (claude plugin marketplace update swaps the clone out and back at the same path).
AK_SRC overrides all of this.
It reinstalls when the source path or any of the three pyproject.toml changes,
takes out any other uv tool that puts ak on PATH, and never runs in a program's session. The vault's and
memory's hooks run on that tool's Python (hooks/akpy.py, canonical here). Inside a checkout, ak re-execs
into the checkout's plugins/ak (uv run --project). ak builds alone — its pyproject names no sibling (uv
locks every group, so a missing ../vault would fail even an optional one) — and a module mount in a
checkout reaches its plugin's own src/, put last on sys.path (cli.py: _reach_sources).
Which checkout runs. The installed ak is an editable install of one checkout
(the release clone, RELEASING.md). Run inside a different checkout of this repo — a worktree — it hands off to
that tree's plugins/ak first (uv run --project, so that tree's own deps apply), and
says so on stderr in a terminal. AK_NO_REEXEC=1 pins the installed one. ak gateway
(and ak xray) follows the same rule for the TypeScript CLI it hands off to: $AK_CODE, else the checkout
you stand in, else ~/agentic-kit, else the release clone — so a worktree runs its own gateway verbs. ak shim
looks for shim/cc-shim.mjs in the same four places, so both work on a machine that has only the release clone. (Installing the service
and restart refuse a worktree: they would put its unmerged code on the wire every session goes
through.)
mv ~/old-name ~/new-name detaches a folder's whole life: Claude Code files its chat history under
a folder named after the path (~/.claude/projects/-Users-x-ak), so /resume and -c come up
empty; git records every worktree by absolute path; the tracer, the trace store, ~/.claude.json,
settings and the memory's caches all name it. ak ws move OLD NEW moves the folder and every
pointer to it, as one journaled, undoable move (src/ak/move/):
ak ws move ~/old-name ~/new-name --dry-run the plan, nothing changed
ak ws move ~/old-name ~/new-name do it
ak ws move --adopt ~/old ~/new you already ran mv: fix everything else
ak moves undo <id> put it all back, byte for byte
Pointers move, history stays. A session folder's name, a ~/.claude.json project key, a
settings path (settings.json and settings.local.json: a value that is the folder, and the folder
inside any string — fileSuggestion.command, a hook's command, an env value — at a path boundary:
not glued to a longer name before it, and followed by /, the end, whitespace, a quote, or one of
; : ) | & , > or a backtick, so cd ~/agentic-kit;make and PATH=/x/ak:/usr/bin move and
~/agentic-kit-wt/x is not under ~/agentic-kit; a folder under $HOME is found as ~/rest too and stays a
~/ form, "~/.ak" → "~/.ak" — written absolute when the new place is outside $HOME; the dry run
names each key), the tracer's project_hash/session_file, the trace store's transcript pointers, the
memory registries (~/.ak/db/workspaces.json, ~/.ak/cache/names.json) are rewritten
(plugins/known_marketplaces.json and installed_plugins.json only where a value is itself a path under
the folder). A transcript's recorded cwd, an observer row's label,
a ledger line, the tracer's i_files are history and are never touched: one line in ~/.ak/db/lineage.jsonl
({"kind":"path"|"label","from","to"}) lets each reader find them under the new name — the
observer's labels_with_aliases and harvest follow it. A worktree's venv (absolute shebangs) and
the folder's memory index are set aside to rebuild; a note at the old path says where it went
(--link leaves a symlink instead) until ak moves finalize.
Safe by construction. It refuses inside Claude Code (CLAUDECODE) and while any Claude Code
session is open (they write the same ~/.claude.json and history), across disks (a move is a
rename, never a copy), onto a destination or a chat-history folder that already exists, and anywhere
near the gateway: its files, its launchd unit and the shim fragment that routes claude at it
are guarded in every op (move/ops.py: protected), and the check after the move undoes it if the
gateway stopped answering. The gateway runs from its own release snapshot under
~/.local/share/ak/gateway/releases/, never from the repo, so moving the repo cannot cut it.
How it runs. plan.py asks each handler what it touches and returns plain JSON steps; the CLI
checks the machine is quiet, writes ~/.ak/moves/<id>/plan.json, copies the engine
(runner.py, ops.py, lineage.py, stdlib only) into that folder and runs it there, so the move
can relocate the checkout the planner ran from. Each step is journaled before and after it runs
(journal.jsonl), every file edited in place is clonefile-copied to backup/ first, and a JSON
edit is undone by key — what Claude Code wrote after the move survives an undo. A step that fails
undoes the lot; a hard crash leaves a journal that moves undo or moves resume finishes, from any
step (tests/test_move.py crashes it after each one). Without the kit:
python3 <move dir>/engine/runner.py undo <move dir>.
A plugin's own data joins through plugin.json: "relocate": {"entry": "scripts/relocate.py", "attr": "handler"} — a stdlib module whose handler.plan(move) adds steps (move.step(handler, name, phase, op, **args) with an op from ops.OPS), findings (move.fail/warn/info) and lineage
rows (move.lineage(kind, from, to)), and whose handler.detect(ctx) feeds ak doctor. Doctor's own
layout check fails while the kit home is still on layout 1 (no ~/.ak/layout saying {"layout": 2}, or
a .store/ left at its root) and names scripts/migrate/4-home-layout.py.
tracer (its index) and observer (a label row, never a row rewrite) ship one.
ak plugin rename tag@old tag@new --dry-run one plugin id: enabledPlugins, installed_plugins.json,
its cache/<m>/<p> and data/<p>-<m> folders
ak plugin rename --marketplace old new a marketplace and every plugin id under it
(+ known_marketplaces.json, extraKnownMarketplaces)
ak home move ~/.new-home the kit's own state home
ak moves undo <id> either, byte for byte
Both plan into the same Move (move/kinds.py), journal the same way, refuse inside Claude Code and
with any session open, and write a lineage row. Registry edits are exact (ops.json_edit: a key
renamed in place, a path re-pointed; undone from the backup when the file is untouched since, else by
key). home move renames the folder on the same disk in one step that also leaves a symlink at the old
path (ops.move_dir) — the journal lives inside the home, and a straggler running old code would
otherwise re-plant an empty one; the link stays until moves finalize. Settings (WIKILINKS_ROOTS and
every other path, ~/ forms kept), ~/.claude.json, chat-history folders of sessions run inside the
home, the registries inside it and Obsidian's vault list (<home>/vault) follow. A shell profile's
$AK_HOME, $AK_PATH or $AK_STORE is a process variable: the plan warns with the new value, you edit it. The gateway is not part of home move yet.
What has run where. macOS is the only OS the kit has been used on. The test suites (memory, observer,
tracer, wikilinks, plugins, agentic-engineering, the gateway and the shim — before the split into core, vault and
memory) have also passed in a real Ubuntu 24.04 (arm64) container; no systemd unit, no live Claude Code session and no service ran there. Native Windows has
never run anything: its cells are backed by code that was read and by unit tests that fake the OS (paths, the
Task Scheduler commands, the registry PATH edit) — treat them as untested on a real machine, and the systemd
cells as untested against a real systemd --user. The one service manager measured for real is the gateway's
launchd backend (gateway/docs/operations.md). The CI matrix for Linux and Windows (.github/workflows/ci.yml) is
manual-only and has not been run. notify is macOS-only by design (a native popup): on other OSes its hooks
exit without doing anything.
| macOS | Linux (systemd) | WSL | native Windows | |
|---|---|---|---|---|
| hooks (every plugin) | uv run starts each one; the vault's and memory's through hooks/akpy.py, which execs ak's Python |
same | same | same; Claude Code runs the command in Git Bash or PowerShell, and the string parses the same in sh, cmd and PowerShell; akpy.py runs ak's Python as a child and passes its exit code on |
ak CLI |
installed on SessionStart by uv tool install --editable (ak, vault, memory); bin/ak |
same | same | same; bin/ak.cmd |
| tracer, observer | yes | yes | yes | yes; see the observer note below |
tool-call index (ak trace) |
node >= 22.13 |
same | same | same |
vault (the [[name]] Read hooks, bin/suggest) |
yes | yes | yes | yes |
| gateway service | launchd (~/Library/LaunchAgents) |
systemd --user unit |
needs systemd switched on in the distro | Task Scheduler task \ak-gateway, at logon |
shim (ak shim install) |
claude sh stub; .zshenv and other rc files |
same, plus ~/.config/environment.d |
same as Linux | claude.cmd; the shim dir goes first in the User Path; only *.mjs fragments run |
| routines login | Keychain, then ~/.claude/.credentials.json |
~/.claude/.credentials.json |
same | %USERPROFILE%\.claude\.credentials.json |
ak tasks, evals/run.py (tmux) |
yes | yes | yes | no: both stop and say to use WSL |
ak memory demo |
yes | yes | yes | no: exits 64 |
ak ws move |
checks for open stores with lsof |
reads /proc, else lsof |
same as Linux | renames each store aside and back: a refusal means another process holds it |
ak xray |
-p and interactive |
same | same | -p only; an interactive session is refused |
CLAUDE_CODE_OAUTH_TOKEN is read first by routines on every OS. Folders follow the OS: macOS and Linux keep
~/.config and ~/.local/share (honouring XDG_*); Windows uses %APPDATA% and %LOCALAPPDATA%
(scripts/BRAND.md, "The platform seam").
What every OS needs, and where it stops short:
uvon PATH. Every hook isuv run .... With nouva hook now fails loudly (non-zero, the shell's own "command not found") instead of doing nothing silently. A machine with no Python 3.10+ and no network cannot get one, and its hooks error (plugins/HOOKS.md, section 1).node>= 22.13 for the tool-call indexer the tracer's Stop hook launches andak tracereads (the tracer's owntraces/bin/traces.mjs, over its committed bundletraces/dist/traces.mjs), and for the shim. The hook andak tracelook for it on PATH, then where nvm, fnm, volta, asdf, Homebrew and the Windows installers put it (plugins/tracer/hooks/convo-index-stop.py,plugins/tracer/src/tracer/calls.py). The gateway's own floor is 22.7 (gateway/package.json). With no node the hook logs "skipped" and the session carries on;ak tracethen has nothing new. Both look past PATH: nvm's newest, fnm, volta, asdf, Homebrew,/usr/local,/usr/bin, and on Windows Program Files, nvm-windows and volta.- Where
ak trace's engine comes from.$AK_CODE/plugins/tracer/traces/, else the tracer plugin's owntraces/— the one shipped copy: the bundle is committed in the plugin, so a tracer installed alone reads and writes the index.npm run buildinplugins/tracer/traces/refreshes it. On macOS/Linux the bash doortraces/bin/tracesruns it (it also finds a node when PATH's is too old); on Windowsnode traces/bin/traces.mjs. tmuxforak tasksand the eval runners: POSIX or WSL only. On native Windows both say so and stop.- Intel Mac and the observer's dense search.
zvec==0.7.0andfastembedcarry platform markers inplugins/observer/pyproject.toml, andplugins/observer/uv.lockhas no macOS x86_64 wheel for them (wheels: macOS arm64, Linux x86_64 and aarch64, Windows amd64). The observer is not blocked there: its venv builds without them and search answers asunion(keywords only). Onlyak observer store embedis unavailable, andak setupsays so instead of running it. This is read from the lock file and the code; it was not run on an Intel Mac. - Native Windows hooks run in whatever shell Claude Code picks there (Git Bash or PowerShell);
hooktrace.pyruns the hook it wraps through Git Bash too (CLAUDE_CODE_GIT_BASH_PATH, elsebashon PATH, else Git for Windows' install folder), and throughcmd.exeonly when there is no bash at all. - The first session on a fresh Windows machine downloads a Python for the hooks'
--python ">=3.10": a hook with a short timeout may time out that once. Runuv python install 3.12first. - Windows console encoding: every
akentry point switches stdio to UTF-8 (andbin/ak.cmdsetsPYTHONUTF8=1), so a cp1252 console or pipe prints✓ → ⚠and emoji titles instead of crashing. - WSL needs systemd for the gateway service: set
[boot]systemd=truein/etc/wsl.conf, thenwsl --shutdown.ak gateway doctorsays so;ak gateway start --detachruns the gateway with no service meanwhile. - Native Windows, what is left out:
ak memory demo(POSIX shell),ak tasksand the eval runners (tmux), an interactiveak xraysession (noscriptto give it a terminal: useclaude -p) and*.shshim fragments.ak ws movetells whether a store is held by renaming it aside and back (Windows refuses to rename a file another process has open), and an npm-installedclaude(anodeprocess) is known by its command line.
When an agent does something odd, the cause is often text a plugin added to its context, or a hook that blocked it. That text lives in
scripts, is often built at runtime, and fires at moments you can't see. ak plugin reads it
back from Claude Code's own record, so you don't have to read every hook.
ak plugin ls every plugin, loudest first, over the last 20 sessions
ak plugin inspect <name> one plugin: each hook next to how often it fired, what it
cost, and what it says most, whole; its skills, agents, MCP
ak plugin log [<name>] one session, turn by turn: everything a plugin printed,
who read it (the model, or only you), and what it cost
ak plugin on its own is ls (list, its released name, still runs). Every verb takes --json. ak plugin rename OLD NEW is a move, in the same group (see Moving a workspace). Until 4.0.0 this was its own plugin, plugins@ak; it is
src/ak/plugin_inspect/ now, so it needs no vault and no other plugin.
Nothing is cut. What a hook said is the reason to run these verbs, so every text is shown whole, in every mode. How it lands depends on who reads it:
| reader | inspect / log |
|---|---|
| a person (a terminal) | a heading per hook (per turn, in log), its whole text in a box under it, line breaks kept, the box coloured by who read it (model · you · a block). list and inspect gather the silent ones on one line, so the ones that spoke stand out |
an agent (a pipe, CLAUDECODE) |
header-then-TSV, one row a hook, every word of its text on that row |
--json |
the text exactly as printed |
inspect shows, per hook, its most frequent text; text built at runtime varies, so texts are
grouped by their opening and the newest one speaks for the group. log drops an interpreter's
path from the front of an error (/opt/…/Python: can't open file …); --raw keeps it.
| question | read from |
|---|---|
| what is installed, on or off | <claude>/plugins/installed_plugins.json, settings.json enabledPlugins (user, then the project's) |
| what each plugin is wired to do | its hooks/hooks.json (and plugin.json hooks), skills/*/SKILL.md, agents/*.md, commands/*.md, .mcp.json |
| what it said | the session transcripts, <claude>/projects/*/*.jsonl: Claude Code writes every hook run as an attachment record (hook_success with command, stdout and durationMs; hook_blocking_error; hook_non_blocking_error; hook_cancelled). A PreToolUse block comes back as the tool's error result, PreToolUse:Write hook error: [<command>]: <stderr> |
| which model call carried it (Claude Code 2.1.300+) | the same transcript: since 2.1.300 it also saves every model call (api-request rows, with the message bodies sent as api-request-blob). The context of one moment's hooks is recorded once more as hook_additional_context, and goes out as a system message, a PostToolUse one inside the tool result. Each text is tied to its hook run and to the first call that carried it |
<claude> is CLAUDE_CONFIG_DIR, else ~/.claude. Nothing is run and nothing is written.
A row is read by the Claude Code version that wrote it (its own version, src/ak/transcript_rows.py): a session
resumed after the upgrade is read the old way up to it and the new way after. log --json gives each line
source (hooks: read off the hook run; record: also found in a saved call) and call (that call's number in the
file, from 1), and a terminal shows call N under the box. Over 300 real sessions (2026-10-15) 96.9% of hook text
was found in a call; the rest is in no saved body (a body Claude Code did not save, or text it never sent), and stays
hooks. Context no hook run on record printed gets a line of its own, named by its words among the plugins with a
hook on that event, else ?.
| kind | who reads it |
|---|---|
context |
the model (additionalContext, or plain stdout on SessionStart / UserPromptSubmit) |
deny · ask · block |
the model: a refusal and its reason |
stderr |
the model: exit 2 after the tool ran (a write-lint), which blocks nothing |
user |
only you (systemMessage) |
error |
no one: the hook failed, Claude Code carried on |
silent |
no one (log --all shows them) |
Tokens are characters ÷ 4, counted only for what reaches the model, and once: every later call re-sends a text, from the prompt cache.
A hook record carries its command exactly as hooks.json wrote it, ${CLAUDE_PLUGIN_ROOT}
unexpanded, so (event, command) names the plugin — looked up in every installed plugin and
every cached version, because a past session ran a past version. Two plugins can run the very
same command (uv run … hooks/session-start.py); then the text's opening words decide, the
candidate whose source holds them. When nothing decides, both are named (alpha|beta),
never guessed.
- A hook that prints nothing is often not recorded at all:
inspectsaysno record, which means silent or never fired.ak hooks onrecords every run with its timing. - Only hooks are read back from transcripts. Skills, agents and MCP servers are listed from their files: what the model is told about them each session, not when it used them.
Bare ak shows one line from it: plugins on, hooks wired, and any hook whose
script is missing (broken hook in <plugin>). That check reads files only.
A plugin that cannot import ak carries a copy of what it needs, stamped and checked by scripts/copies.py
(where ak lists them): src/ak/ui.py the one output door and src/ak/groups.py the command groups (observer,
tracer: plugins/AGENTS.md §6), src/ak/session_origin.py the person-or-program rule, src/ak/transcript_rows.py
which transcript rows are chat and which Claude Code version wrote one (the observer's hooks), src/ak/move/lineage.py,
hooks/akpy.py the vault's and memory's hook launcher. bin/ak and its Windows twin bin/ak.cmd are themselves
copies of scripts/seam/launcher: a launcher runs the console script it is named for, so every plugin's bin/<name>
is the same file.
uv run pytest -q: the output contract (test_cli_output.py), the mount contract, moves, trace, ak plugin
against a fake Claude Code home (test_plugin_inspect.py), the brand seam, the install hook against a fake uv.
After a Claude Code update, AK_TEST_REAL_TRANSCRIPTS=1 uv run pytest -q tests/test_transcript_rows_real.py checks the
rules against this machine's own transcripts: every line's type read right, no row type the kit does not know, and
at least 95% of hook text tied to the call that carried it. A failure there is a change in what Claude Code writes
that a reader must follow (a FEATURES entry or a KNOWN_ROW_TYPES name in transcript_rows.py, and a branch in
the reader).