Claude

Agentby Tori @gettoriApache-2.0Bundled
Verified against
claude 2.1.231Measured Aug 14, 2026
Platforms
macOS, Linux, Windows
Install
Tori installs it with npmnpm install -g @anthropic-ai/claude-code
Needs Tori
26.1009.0 or newer
Schema
Version 5
Id
claude
Source file
agents/claude.toml ↗gettori/packs on GitHub

Add it in Tori

  1. Open Settings with the gear in the title bar.
  2. Go to Agents.
  3. Claude is already there: it ships inside Tori. When the catalog has a newer version, Add an agent lists it with Update.

Adding writes one file and runs nothing. Installing it with npm is a separate click on its card.

Use it without the catalog

Download the file and drop it in the folder below. Tori loads it the next time it starts.

Download file claude.toml
~/.config/tori/packs/agents/

Chat and accounts

ChatChat

Tori opens a chat pane next to the terminal and talks to Claude through its own protocol.

AccountsMore than one

Sign in to more than one account and choose which one each session uses.

Usage readoutsShown

Tori shows remaining usage for each account in the sidebar.

The file

claude.tomlRead only
1schema_version = 52id = "claude"3label = "Claude"4description = "Claude Code, with chat over its stream-json protocol"5license = "Apache-2.0"6contributor = { name = "Tori", github = "gettori" }7icon = "claude"8# The CLI these conventions were empirically captured against. The chat9# transport below is a measurement of this version's wire format, so a drift10# warning against a newer binary is meaningful rather than cosmetic.11#12# Left at 2.1.231 on purpose: only the MCP surface (`--mcp-config` beside13# `--settings`, `--strict-mcp-config`) was re-measured, on 2.1.278 and 2.1.28014# with dev/mcp-probe.mjs. The rest of this file was not.15verified_against = "claude 2.1.231"16verified_on = "2026-08-14"1718[launch]19program = "claude"20base_args = []21yolo_args = ["--dangerously-skip-permissions"]22resume_args = ["--resume", "{id}"]2324[discovery]25dir = "~/.claude/projects"26filename_pattern = '^(?P<id>.+)\.jsonl$'2728[parser]29kind = "claude_jsonl"3031[running]32# ERE for `pgrep -f`; matches a live claude process driving this session id, not33# merely any process whose command line contains the id (a `less`/`tail` on the34# transcript file, an editor with it open).35#36# The `([^ ]+ )*` is load-bearing, and was measured rather than assumed. The37# original `claude (--resume|-r) {id}` required the flag to sit immediately38# after the program name, which is true of a PTY agent tab (`claude --resume39# <id>`) but false of **every chat session**: the chat transport puts its40# base_args first, so the real command line is `claude -p --input-format41# stream-json ... --resume <id>`. Chats were therefore invisible to42# `session_running`, and so to every guard built on it. A new chat was doubly43# invisible, since it is started with `--session-id` and never `--resume`.44#45# Matching a run of whole argument tokens keeps the decoys out: a `tail` on46# `~/.claude/projects/<p>/<id>.jsonl` has no flag token before the id.47pattern = 'claude ([^ ]+ )*(--resume|-r|--session-id) {id}'4849[capabilities]50# Empirically confirmed (2026-07-18, phase 2 capture): a real permission51# prompt (`--permission-mode plan`, "Would you like to proceed?") leaves the52# PTY fully silent for 9s+ while waiting; a 20s Bash tool run stays noisy53# throughout (spinner redraws every <=0.62s). 2000ms sits well above the54# noisy-phase gaps and far below the observed blocked-silence duration.55pty_quiet_ms = 200056needs_you = true57# Verified (2026-07-18, phase 3 spike + capture): `--settings <inline JSON>`58# injects a `Notification`/`UserPromptSubmit`/`PreToolUse`/`Stop` hook set59# without touching ~/.claude/settings.json (see crate::hooks); Notification60# fires when claude itself decides it needs the user (a permission prompt or61# an idle nudge), replacing the quiet-PTY x pending-tool_use guess with a62# ground-truth signal for Tori-launched sessions only.63hooks = true6465# --- schema v3: accounts ---66#67# Measured 2026-08-14 against the CLI named in `verified_against`, not read68# from documentation.69#70# `supports_isolation = true` is earned, not assumed: two profiles held71# simultaneous logins for the same account, because claude namespaces its72# macOS Keychain service by config dir73# ("Claude Code-credentials-" + sha256($CLAUDE_CONFIG_DIR)[:8], with the74# default home taking the unsuffixed name). A second login therefore cannot75# sign the first one out. That measurement is darwin-only; Linux has no login76# Keychain and has not been measured.77#78# The hash is taken over the raw environment string rather than a resolved79# path, so ".../home" and ".../home/" are two different logins for one80# directory. Tori canonicalizes before setting `home_env` for exactly that81# reason.82#83# `login_args` runs in a real PTY and never headlessly: `claude auth login` is84# browser OAuth with no non-interactive variant, and `setup-token` is85# interactive too, so a captured login would hang rather than fail.86#87# `whoami_args` is the one probe Tori runs captured, and it is safe to: measured88# both ways, `claude auth status` writes JSON to stdout and exits 0 signed in, 189# signed out. It also names the account (`email`) and, when one is inherited,90# the environment variable supplying an API key (`apiKeySource`), which is where91# the billing-override warning comes from rather than from Tori sniffing its own92# environment.93#94# `logout_args` is non-interactive too: measured against an isolated home with95# nothing signed in, it printed "Successfully logged out" and exited 0.96#97# `home_default` is where CLAUDE_CONFIG_DIR points when it is unset, and it is98# what makes a second account's history findable: measured in Phase 0, a session99# run under an isolated home wrote its transcript to100# "<isolated home>/projects/<slugified-cwd>/<uuid>.jsonl" and the shared101# ~/.claude/projects gained nothing. So a profile's root is the `dir` below with102# this prefix swapped for that profile's home, and the layout under it is the103# same one.104#105# `home_markers`: an adopted folder must be empty or already hold one, since the106# add-time `whoami` writes into whatever it is pointed at. Measured 2026-09-14:107# `claude auth status` on an empty CLAUDE_CONFIG_DIR wrote `.claude.json` there.108[accounts]109home_env = "CLAUDE_CONFIG_DIR"110home_default = "~/.claude"111home_markers = [".claude.json", "settings.json", "projects"]112login_args = ["auth", "login"]113logout_args = ["auth", "logout"]114whoami_args = ["auth", "status"]115whoami_kind = "claude_json"116supports_isolation = true117# The interactive CLI runs its first-run wizard (theme, then login) whenever118# this key is unset, credentials or not. `auth login` never sets it.119onboarded_file = ".claude.json"120onboarded_key = "hasCompletedOnboarding"121plugins_kind = "claude_installed_json"122123# --- schema v4: the usage source ladder ---124#125# Declaration order is the ladder's order, and the first entry is what an agent126# resolves to when the user has chosen nothing.127#128# `sessions` first because it is free: `rate_limit_event` already rides the chat129# transport, so the reading costs nothing and needs no extra process, and it is130# what an account nobody has answered for resolves to.131#132# `token` is the deeper rung and the only route to the model-scoped weekly133# window, which no passive frame carries. It reads this account's own OAuth token134# out of the login Keychain, so it is never a default: the user turns it on135# knowing macOS will ask. `cli` stays undeclared because claude has no136# non-interactive usage command; that gap is the CLI's, not a phase Tori owes.137[usage]138sources = ["sessions", "token"]139140# --- schema v5: the files claude reads out of an account home ---141#142# Every `path` is relative to the account home: `[accounts].home_default` for143# the default account, that profile's own home for any other. Measured against144# 2.1.263, by reading the shipped binary's own loader rather than the docs:145#146#   * `rules` is read **at user level**, not only per project. The memory loader147#     pushes `gpe({rulesDir: fge(), type: "User"})` alongside the user CLAUDE.md,148#     where `fge()` is `join(<user config dir>, "rules")`, and the walk takes149#     `*.md` recursively. This row is here because that was measured; it was to150#     be left out otherwise, since a row for a directory nothing reads is worse151#     than no row.152#   * `skills` is a **folder** per skill: `skills/<name>/SKILL.md`, which the153#     CLI's own help states in those words.154#   * `agents` and `commands` are **single files**: the CLI's scaffolder writes155#     `agents/example.md`, and its help calls commands `~/.claude/commands/*.md`.156#     So a subagent is `agents/<name>.md`, not a folder, which is why `new_path`157#     is declared per row rather than inferred from the row's id.158#159# `~/.claude.json` is deliberately absent: it is live state (session cursors,160# project history), not something a person edits.161[config]162163[[config.entries]]164id = "instructions"165label = "Instructions"166path = "CLAUDE.md"167kind = "file"168169[[config.entries]]170id = "skills"171label = "Skills"172path = "skills"173kind = "dir"174new_path = "{name}/SKILL.md"175new_name_hint = "skill-name"176template = '''177---178name: {name}179description:180---181'''182183[[config.entries]]184id = "commands"185label = "Commands"186path = "commands"187kind = "dir"188new_path = "{name}.md"189new_name_hint = "command-name"190191[[config.entries]]192id = "agents"193label = "Subagents"194path = "agents"195kind = "dir"196new_path = "{name}.md"197new_name_hint = "subagent-name"198template = '''199---200name: {name}201description:202---203'''204205[[config.entries]]206id = "rules"207label = "Rules"208path = "rules"209kind = "dir"210new_path = "{name}.md"211new_name_hint = "rule-name"212213# Seeded with an empty object rather than an empty file: claude parses this one214# as JSON and reports a zero-byte file as a settings error.215[[config.entries]]216id = "settings"217label = "Settings and hooks"218path = "settings.json"219kind = "file"220template = "{}\n"221222# --- schema v2: the structured chat transport ---223#224# Everything below was measured against the CLI named in `verified_against`,225# not read from documentation; `dev/protocol-probe.mjs` re-verifies the wire226# format these args produce.227#228# NOTE: every scalar key here must stay above the first [[chat.*]] header. A229# TOML key written after a table header belongs to that table, so moving one230# down silently reparents it.231[chat]232transport = "claude_stream_json"233# `-p` with stream-json both ways, and stdin held open for the life of the234# session. Closing stdin is what made early probes exit after a single turn;235# the CLI itself is happy to run many.236base_args = [237  "-p",238  "--input-format", "stream-json",239  "--output-format", "stream-json",240  "--verbose",241  "--include-partial-messages",242  # Puts the user's own hooks in the transcript in-band. Tori's injected243  # approval hook rides the same stream and is folded away by default (it244  # stamps its own output so it can be told apart; `hook_name` reports the245  # tool, not the matcher, so the name cannot identify it).246  "--include-hook-events",247  # Permission questions come back in-protocol as a `can_use_tool`248  # control_request that Tori answers with a `control_response`, instead of249  # Tori deciding for itself in a `PreToolUse` hook. This is what makes the250  # agent's own permission modes real: `default` asks, `acceptEdits` and251  # `bypassPermissions` do not, and Tori renders whatever the CLI decided to252  # ask rather than overriding it.253  #254  # Undocumented on purpose, it seems: the flag is absent from `claude --help`255  # on 2.1.231, yet it parses (an unknown flag exits with "unknown option") and256  # a `default`-mode `Write` produces the control_request. So this is a257  # measurement of a working but unlisted flag, which is exactly the kind of258  # thing `verified_against` above exists to re-check on a version bump.259  "--permission-prompt-tool", "stdio",260  # NOT `--settings {"fastMode":true}`, and the reason is worth keeping.261  #262  # It was here for one commit, on the reading that the flag was an *opt-in*:263  # without it a `-p` session reports `fast_mode_disabled_reason:264  # "sdk_opt_in_required"`, and passing it moves the reason on to whatever the265  # account says. That much is measured and true. What it is not is inert. The266  # CLI's own settings schema reads "When true, fast mode is enabled. When267  # absent or false, fast mode is off", so on any account that *can* serve fast268  # mode, this turned it on for every chat Tori opened.269  #270  # It looked harmless only because the machine it was measured on has usage271  # credits disabled, so `fast_mode_state` stayed `off` and the flag appeared to272  # do nothing. A measurement that agrees with a wrong reading is the most273  # expensive kind.274  #275  # Fast mode is a thing the user turns on, and it costs them credits, so it276  # belongs on a control rather than in the args every chat is spawned with.277]278session_id_args = ["--session-id", "{id}"]279resume_args = ["--resume", "{id}"]280# Forking replays an existing conversation into a **new** session id. Measured:281# `--resume <old> --fork-session --session-id <new>` honours the id we pass and282# reports it back, which is what lets Tori claim the fork before starting it -283# a CLI-chosen id would be claimed only after the fact, if at all. `{from}` is284# the session being forked; `{id}` is the new one, and the template carries both285# because a fork is the one case where the two differ.286fork_args = ["--resume", "{from}", "--fork-session", "--session-id", "{id}"]287model_args = ["--model", "{model}"]288effort_args = ["--effort", "{effort}"]289mode_args = ["--permission-mode", "{mode}"]290add_dir_args = ["--add-dir", "{dir}"]291# Measured on 2.1.285: `-p` shows no workspace trust dialog, so a folder's own292# `.claude/settings.json` hooks run on the first message. `user` leaves them out293# and keeps the user's settings and Tori's `--settings` file.294untrusted_args = ["--setting-sources", "user"]295296# No [[chat.models]]. **Tori declares no model this CLI did not name.**297#298# There used to be a table of four here, carrying labels, context windows and299# effort levels. It was the picker's fallback and the meter's pre-first-turn300# denominator, and it was wrong at both. Sonnet 5 and Opus 5 both said 200000301# while the agent reported 1000000 for each (dev/fixtures/claude/plain-turn.jsonl302# and fast-mode.jsonl, provider `firstParty`), and Fable's figure was inferred303# from the `[1m]` suffix on a catalogue value by a build that had never run a304# Fable turn. A session that never handshook offered all four regardless of what305# the installed CLI could actually run.306#307# All of it comes from the agent now: the model list and the effort levels from308# the `initialize` control response (live, or cached by `catalog_probe`), the309# context window from a completed turn's `result.modelUsage`. Nothing renders310# before one of those has spoken, which is the point.311#312# `[[chat.annotations]]` used to survive here, carrying one model's fast mode.313# It is gone too: the handshake publishes `supportsFastMode` per model, so the314# table was restating the CLI in a spelling the CLI does not use315# (`claude-opus-5` against a resolved `claude-opus-5[1m]`) and matched nothing.316317# The six permission modes `--permission-mode` accepts and honours, and they now318# mean what they say: Tori's hook no longer decides anything, so nothing runs319# ahead of them and `bypassPermissions` bypasses permissions.320#321# `manual` is deliberately absent. The CLI accepts it, but its own help calls it322# "an alias for 'default'" and the wire agrees: `--permission-mode manual`323# reports `permissionMode: "default"` at init. A row for it would be a second324# way to pick a mode already on the list.325#326# Hints are the CLI's own wording, from the `--permission-mode` description it327# embeds. A mode says what the agent may do without asking, so the description328# is quoted rather than improvised.329[[chat.modes]]330id = "default"331label = "Ask"332hint = "Standard behavior, prompts for dangerous operations."333args = ["--permission-mode", "default"]334# What a session runs when nothing is chosen, and what an unresolvable mode335# downgrades to. Marked rather than assumed: "default" is Claude's spelling,336# and the resolver that reads this must not carry it to another agent.337default = true338339[[chat.modes]]340id = "acceptEdits"341label = "Accept edits"342hint = "Auto-accept file edit operations."343args = ["--permission-mode", "acceptEdits"]344345[[chat.modes]]346id = "plan"347label = "Plan"348hint = "Planning mode, no actual tool execution."349args = ["--permission-mode", "plan"]350351[[chat.modes]]352id = "auto"353label = "Auto"354hint = "Use a model classifier to approve/deny permission prompts."355args = ["--permission-mode", "auto"]356# Measured: a model that does not declare `supportsAutoMode` (haiku declares357# none of the capability flags) accepts this flag, exits 0, and silently runs358# `default` instead. Nothing on the wire objects, so the row is hidden here359# rather than offered and quietly ignored.360requires = "supportsAutoMode"361362[[chat.modes]]363id = "dontAsk"364label = "Don't ask"365hint = "Don't prompt for permissions, deny if not pre-approved."366args = ["--permission-mode", "dontAsk"]367368[[chat.modes]]369id = "bypassPermissions"370label = "Bypass"371hint = "Bypass all permission checks."372args = ["--permission-mode", "bypassPermissions"]373# Runs every tool without asking anyone. Not the retired `permissive_caveat`374# wearing a new name: that declared a warning about *Tori*, which stopped being375# true when Tori stopped gating. This declares what the mode itself does, which376# was always true and matters more now that nothing catches it afterwards. Only377# this mode: `acceptEdits` still asks about everything that is not an edit, and378# `dontAsk` denies rather than runs.379permissive = true380381# No [[chat.effort]] either, and for the same reason [[chat.models]] went. It382# listed the five levels `--effort` accepts, which is a list the agent already383# publishes per model (`supportedEffortLevels`) and publishes better: haiku has384# none, and the table offered it all five. `effort_args` above says how to spell385# a level, for whichever levels the catalogue turns out to name.386#387# What is left is the opposite kind of claim: a level the CLI accepts and never388# mentions. `--effort --help` advertises low/medium/high/xhigh/max, and the389# validator quietly takes one more.390#391# Measured by dev/effort-probe.mjs, first on 2.1.237 and re-taken unchanged on392# 2.1.238, which is the version the row below names. The observable is the CLI's393# own validator: `--effort ultracode --version` writes no "Unknown --effort394# value" warning, where `off`, `ultra`, `ultrathink`, `none`, `minimal` and `auto` all395# do. That is this CLI asserting the word is one of its own, which is a396# different thing from the `--permission-mode auto` trap, where a flag is397# accepted on a model that ignores it and nothing anywhere says so.398#399# A behavioural second observable was built and measured and does not work: the400# thinking-token count is the model's discretion, and paired runs gave `off`401# 466/0/0 against `ultracode` 0/0/1633. The probe's header carries the numbers402# and the reasoning; the short version is that a check flipping on model mood is403# a check nobody reads.404#405# `measured_on` is what keeps this honest. It is a claim about a binary, so it406# holds for that binary and degrades to a disabled row naming both versions on407# any other, rather than silently outliving the measurement.408[[chat.effort_extras]]409id = "ultracode"410label = "Ultracode"411state = "working"412measured_on = "2.1.238 (Claude Code)"413414[install]415# npm names the package that ships the launch binary, and all three verbs were416# run on macOS. The binary lands in npm's global bin, which has to be on the417# login shell's PATH for Tori to find it.418program = "npm"419args = ["install", "-g", "@anthropic-ai/claude-code"]420update_args = ["install", "-g", "@anthropic-ai/claude-code"]421uninstall_args = ["uninstall", "-g", "@anthropic-ai/claude-code"]