Rove

Configuration

Most settings are written for you by the Settings dialog. Press ctrl+a, then ,. This page is for when you want to edit them by hand.

Saved repository lookups and per-repository init overrides match equivalent Windows path spellings. A repository saved with backslashes can be selected or configured using Git-style forward slashes without creating a second entry.

Where things live

PathWhatWritten by
~/.config/rove/state.jsonAll your preferences, as flat JSONRove (Settings, CLI); yours to hand-edit
~/.rove/secrets.jsonCredentials Rove holds for you (today: the tier classifier's API key)Settings → Auto routing; not part of state.json, and never printed
~/.rove/themes/*.jsonInstalled themesrove theme add, or drop files in
~/.rove/settings/keybindings.yamlKeybinding overridesYou only
<repo>/.rove/init.sh + init-prompt.mdPer-repo worktree setupYou (committed to the repo)
<repo>/.rove/pr-instructions.mdPer-repo PR action promptYou (committed to the repo)
<repo>/.rove/clone-dirsPer-repo list of ignored directories to clone into new worktreesYou (committed to the repo)

Setting ROVE_HOME_DIR changes the home beneath all these paths. Only ROVE_* environment variables are read. New runtime files use canonical names; a live pre-rename PTY host can still be attached until it exits or rove reset runs. See runtime state.

Runtime path overrides

ROVE_HOME_DIR already decides where the daemon and the PTY host put their socket and pidfile, so most people never touch these. Four variables move one file each, for the case the home cannot cover: running a second Rove beside the one you use, without the two finding each other.

VariableMoves
ROVE_DAEMON_SOCKET_PATHThe socket the daemon listens on, and clients connect to
ROVE_DAEMON_PID_PATHThe daemon's pidfile (what rove daemon stop reads)
ROVE_PTY_SOCKET_PATHThe PTY host's socket — a named pipe on Windows
ROVE_PTY_PID_PATHThe PTY host's pidfile

Each has a ROVE_-prefixed fallback (ROVE_DAEMON_SOCKET_PATH, and so on); when both spellings are set, the ROVE_ one wins. Unset ones stay derived from the home.

Set them as a group, in the same command as ROVE_HOME_DIR. Isolating the home alone still leaves the two processes on the paths they were given, and a half-isolated instance either refuses to start (already served by the daemon on …) or, worse, drives the terminals of the instance you are using:

env ROVE_HOME_DIR=/tmp/scratch-home \
    ROVE_DAEMON_SOCKET_PATH=/tmp/scratch-home/daemon.sock \
    ROVE_DAEMON_PID_PATH=/tmp/scratch-home/daemon.pid \
    ROVE_PTY_SOCKET_PATH=/tmp/scratch-home/pty.sock \
    ROVE_PTY_PID_PATH=/tmp/scratch-home/pty.pid \
    rove daemon restart

A socket path that is too long for the platform is shortened automatically; a pidfile path is used as given. See Troubleshooting for what a half-applied override looks like from the outside.

Editing settings

rove config          # open state.json in your editor
rove config --path   # just print the path

rove uses your configured editor (editor.kind below) unless it is auto (the default), which honors $VISUAL / $EDITOR, then the first installed of nvim, vim, emacs, nano.

Restart Rove to apply a hand edit everywhere.

Hand-editing is safe. Unknown keys are ignored and bad values fall back to defaults, so a typo can't wedge the app; worst case a preference resets. If the file becomes invalid JSON, Rove renames it to state.json.corrupt-<timestamp> and starts fresh rather than deleting it. Rove serializes each complete read, mutation, and atomic write with the state-file lock, so concurrent processes changing different keys preserve both changes. A whole-state reset takes the same lock and intentionally replaces all keys. Hand edits do not participate in this lock; finish editing before changing settings in another Rove process.

Corruption backup also takes the write lock and re-reads the file before renaming it. A reader that cannot immediately acquire the lock returns defaults without moving the file, allowing an active writer to finish its repair. Writers wait up to five seconds for contention and then report failure. The UI retains unsuccessful dirty-key patches for its next flush; CLI writes report the error instead of claiming the setting was saved.

Settings reference

Keys not listed here are internal UI state (saved repos, tab layouts) that happen to share the file. Three exceptions worth knowing: repoConfigs is what rove repo set writes (a map of git toplevel → {initScript, initPrompt}, see Per-repo init); lastSelectedVendor is the legacy engine fallback below defaultVendor; and externalWorktreeSync is not a setting you configure but a cleanup marker Rove writes — it records where the retired worktree-sync hook was once installed so the next launch (or rove hook cleanup) can remove it, flipping to "off" once cleaned.

Appearance

Settings → General groups theme, mode, transparency, focus accent, split style, folded rail, tab row height, working border, task colors, state glyphs, colorblind diff colours, and running title below a workspace preview. Each row opens a selection list with the same preview above it. Use j/k or arrows to preview a choice, enter to save it, or esc to cancel. Previewing never changes saved settings. Clicking an option saves it immediately. The preview uses sample tasks and files; it does not show your sessions.

KeyTypeDefaultWhat it does
activeThemetheme name"claude"See Themes
themeModedark | light | auto"dark"Which half of a theme's { dark, light } colors Rove draws. auto asks the terminal for its background (OSC 11) and switches when the terminal reports an appearance change, so a terminal that follows the OS light/dark setting takes Rove with it. A terminal that never answers leaves auto on dark. A theme without light colors looks the same in both modes
transparentBackgroundbooleantrue, false on WindowsLet the terminal background show through. In transparent mode Rove detects the terminal's actual background (OSC 11) and adjusts body, muted, and host-backed warning text to stay readable on it. Warning text on opaque dialogs and controls keeps the theme color. No setting is needed. Windows starts opaque because Windows Terminal ships acrylic and background images on by default, and a transparent Rove has no opaque surface to scrub stale glyphs against — set it to true to turn transparency on there, and a value you have already chosen is never overwritten
focusAccentprimary | success | infoprimaryColor of the focused-pane indicator
appearance.splitStylebox | line | rail | ruleboxbox frames each split; line is the minimal tmux-style look; rail marks each split with a left accent bar; rule draws a top rule over each split with its name at the right end. The focused split's edge takes the focus accent
sidebar.tabRowHeight1 | 212 gives each agent tab a second line naming the running engine and, when the task pins a reasoning level that engine declares, the level behind a fill glyph that runs ○ (lowest) to ◉ (highest) along the engine's own level list
appearance.workingBorderflow | stillflowWhile the selected task's engine is running, flow runs a colour gradient around the workspace pane's border (derived from the theme accent) and names the task on its top edge; still keeps the plain focus border
appearance.taskColorson | offonon gives every task its own hue, picked from the task id so it stays the same across restarts: the flowing working border runs in the selected task's hue, and each task's sidebar rows carry a one-cell mark in it. The hue takes its lightness and strength from the theme accent and keeps away from the success, warning and error hues. off uses the theme accent everywhere
appearance.glyphSetbraille | starburst | asciibrailleThe marks the task rail, tab strip, Inbox and PR chip draw: braille spins ⠋⠙⠹… beside ○ ● ? !; starburst spins ✻✼❉❊✺✹✸✶ beside ✧ ✦ ? ❢ and needs a font with the Dingbats block (without one, macOS falls back to glyphs of different widths and the rows jitter); ascii uses | / - \ beside o * ? ! for terminals and fonts without Unicode symbols
appearance.colorblindoff | onoffon turns the theme's added colour 60° around the hue wheel toward blue, at lower chroma, so added and removed stop being a red/green pair. Applies to the diff view, the Files pane's A/D letters and +N/-M counts, and the sidebar's +N −M chips; removed keeps the theme colour
appearance.runningTitleshimmer | stillshimmerWhile a tab's engine is running, shimmer sweeps a light band (theme muted text toward the primary colour) across its title in the sidebar at a fixed 30 cells per second; still keeps the plain muted title
localeen | zhenUI language
hints.keyboard.enabledbooleantrueKeyboard discoverability hints
hints.keyboard.prefixTapPresentationlocal | guidelocalOne tap of the prefix key always opens the full keyboard guide. This picks what comes with it: local also shows shortcut badges beside the clickable controls already on screen, guide hides those badges

Turning keyboard hints back on relights the first-use pane hints you'd already dismissed.

Editor

editor.kind and editor.customCommand control the file tree's enter action and rove config. Opening an entire worktree (o in the sidebar or ctrl+a o) uses the separate GUI/workspace opener described below.

KeyTypeDefaultWhat it does
editor.kindauto | vim | nvim | nano | emacs | customautoauto honors $VISUAL/$EDITOR, then auto-detects
editor.customCommandstringunsetCommand for custom, e.g. code -w

In editor.customCommand, {file} is replaced by the quoted file path. Without it, the path is appended.

Set ROVE_OPEN_EDITOR to choose the GUI editor for a whole worktree, for example ROVE_OPEN_EDITOR=zed. Without that variable, Rove tries the code, cursor, windsurf, and zed CLIs in that order, then the platform opener. These variables do not change the file tree's per-file TTY editor.

The Files pane watches the worktree so edits appear without a keypress. Set ROVE_FILETREE_WATCH=0 to turn that watcher off — worth doing on a repo large enough that a recursive watcher costs more than the staleness it removes. With it off, r is the only thing that repopulates the list.

Engines

KeyTypeDefaultWhat it does
defaultVendorengine id"claude"Default engine for new tasks
engineCommand.<id>stringbuilt-inLaunch command, e.g. "engineCommand.claude": "claude --model opus"
engineName.<id>stringbuilt-inDisplay name
customEngineIdsstring[][]Your own engines; see Custom engines
engineProtocol.<id>built-in engine idunsetAdapter a custom engine borrows; see Custom engines
lastActiveVendor.<repo>engine idunsetPer-project last used; outranks defaultVendor. Written by Rove
autoRouting.<tier>.engineengine idclaude for all threeWhat the swift / standard / deep depth launches. Set from Settings → Auto routing; an empty string switches auto routing off (no tier is guessed)
autoRouting.<tier>.modelstringsonnet / opus / fableModel for that depth, in the engine's own spelling; empty = the engine's default
autoRouting.<tier>.effortstringunsetReasoning level for that depth, one the engine declares; empty = the engine's default
autoRouting.classifieroff | jev | an http(s):// URLoffWho picks the tier for rove api add --tier auto. See below — anything else, a typo included, reads as off
autoRouting.classifierThresholdnumber, 0–10.5Confidence below which no tier is picked. A value outside the range is refused and the default used
autoRouting.classifierTimeoutMsnumber, 200–600004000How long to wait before giving up on the classifier. Outside the range, the default
autoRouting.classifierModelstringjev-latestModel id for jev. Pin a version (e.g. jev-1.13.0) to stop a silent upgrade
autoRouting.classifierEndpointstringunsetThe custom endpoint Settings remembers while the classifier points elsewhere. Not read by the classifier — autoRouting.classifier is
autoRouting.classifierKeyEnvstringTYPESAFE_API_KEYEnvironment variable holding jev's key. The token itself never goes in state.json
autoRouting.classifierCustomKeyEnvstringunsetEnvironment variable holding a custom endpoint's key. Unset = no Authorization header is sent

Launch commands are parsed shell-ish, so quotes group arguments. Clear both engineName.<id> and engineCommand.<id> to reset an engine to its default.

These three keys were called autoEffort.<tier>.* up to v0.9.220, and the feature was called Auto effort. Rove moves the old keys to the new names once, at the first launch after upgrading, and deletes the old ones — a table you had retargeted keeps launching exactly what it launched before, and there is nothing to do by hand. If you had both spellings in the file (an older Rove run in between, say), the autoRouting.* value is the one kept.

The tier classifier

autoRouting.classifier is off, and while it is off nothing leaves your machine. Switching it on means one thing you should decide deliberately:

Your prompt is sent to a third party that is not your engine vendor. With jev, the first message of the task — trimmed to 1,200 characters — is POSTed to TypeSafe System One (https://api.typesafe.ai/v1/systemone). That is a different company from whoever runs the engine you picked, and it sees the text before the engine does.

It answers one question — how much of the PROCEDURE the prompt leaves for the model to work out — and maps the answer onto swift / standard / deep: procedure given is swift, goal given but not the procedure is standard, and a goal that still has to be found is deep.

{
  "autoRouting.classifier": "jev",          // off by default
  "autoRouting.classifierThreshold": 0.5,   // below this, no tier is picked
  "autoRouting.classifierModel": "jev-1.13.0"
}
export TYPESAFE_API_KEY=...   # keys: https://console.typesafe.ai/keys
rove api add --repo ~/code/app --tier auto --prompt "there's a memory leak somewhere"

Settings → Auto routing carries all of this as rows — Classifier (off / jev / custom), Endpoint, Confidence floor, API key — with the data-flow sentence above them in every mode, and a line saying where the key is coming from, which is the usual reason a switched-on classifier appears to do nothing. Everything below is the same settings by hand.

Where the key lives

Two places, in this order (and $TYPESAFE_API_KEY below means whichever variable the current mode reads):

  1. The variable in the environment — wins whenever it is set, so a one-off TYPESAFE_API_KEY=… rove … and a CI secret behave the way you would expect. An empty value counts as unset, not as "deliberately blank".
  2. ~/.rove/secrets.json — what Settings → Auto routing → API key writes. It holds nothing but secrets and is deliberately NOT state.json: that file is opened by rove config, hand-edited, and pasted whole into bug reports, which is no place for a live key.

The stored key is the only way a running TUI can have one: it is a long-lived process, so an export typed after it started never reaches it. Settings shows the last four characters of a stored key and never the key; submitting the field empty clears it, and clearing the last key removes the file. A secrets.json that no longer parses is renamed to secrets.json.corrupt-<timestamp> on the next save rather than overwritten, so the keys still inside it can be recovered by hand. Rename the variable with autoRouting.classifierKeyEnv and both places follow the new name.

On Windows, 0600 is not what protects that file. Rove writes it with owner-only permissions, and on macOS and Linux that is enforced. Node maps POSIX mode bits onto the read-only attribute on Windows; who may open the file is decided by the ACL it inherits from your user profile. That is the same protection every other per-user file there has, and it is weaker than the guarantee on the other two platforms — if that matters for your threat model, keep the key in the environment instead.

Point autoRouting.classifier at your own endpoint instead and Rove POSTs {"text": "…"} and expects {"tier": "swift|standard|deep", "confidence": 0.0–1.0} back — that is the whole contract, so an endpoint you host (a local model, a rule, a lookup) is a ten-line program.

http:// is accepted only for a loopback address (127.0.0.1, localhost, ::1), because a classifier you run on your own machine is the whole point of the custom option and nothing leaves the host. To anywhere else, plain http would carry the task's first 1,200 characters — and your bearer token, once you name one — in cleartext to a host anyone on the path can impersonate, so it is refused and .tierAuto says so.

The two modes read different key settings, and that is deliberate. jev reads autoRouting.classifierKeyEnv; a custom endpoint reads autoRouting.classifierCustomKeyEnv, which has no default, so a custom endpoint gets no Authorization header until you name one. A single shared variable would leak in both directions across a mode switch: a name chosen for your own endpoint would send that credential to TypeSafe the moment the setting changed to jev, and TypeSafe's token would go to your endpoint the moment it changed back. Neither is a mistake you could watch yourself make.

Nothing here can fail a create. Off, no key, no network, a timeout, a malformed answer, a refused endpoint, or a confidence under the threshold all mean the same thing: the task is created with the engine fields it would have had anyway, and rove api add reports what happened in .tierAuto. A pick below the threshold is dropped on purpose — a wrong pre-fill costs more than no pre-fill, because someone has to notice it before they can undo it.

The judgement Rove sends is the one that was measured: 73.0% on 94 human-labelled prompts against a 48.9% floor, with deep recall 17/20. It is generated from design/auto-routing/ rather than written in code, so the text a reviewer reads is the text on the wire. What the numbers mean and what else was tried: design/auto-routing-classifier.md and design/auto-routing-classifier-interface.md.

Terminal and tabs

KeyTypeDefaultWhat it does
terminal.scrollbackRowsnumber1000History per embedded terminal. Clamped 100–100,000
chat.tabStrip.modealways | multipleOnly | neverneverHorizontal chat tab strip

The tab strip is off by default: the sidebar tree already lists every tab and marks the active one, so the strip spends a row of the content pane saying what the tree says for free. always shows it, multipleOnly shows it only once a task has more than one tab. (An older chat.tabStrip.hideSingle boolean still works if you set it before chat.tabStrip.mode existed; writing the new key retires it.)

Scrollback changes apply to terminals started after the change; live ones keep the buffer they were born with.

Notifications

All three default to on.

KeyTypeWhat it does
notifications.toast.enabledbooleanIn-TUI completion toasts
notifications.sound.enabledbooleanChime when a background tab finishes
notifications.sound.volumenumberChime level, 0-1 (default 0.4)
notifications.crossTask.enabledbooleanToasts for tasks you aren't looking at

Error toasts always show, even with toasts off. See Notifications for how they're delivered.

Zen mode

Zen hides the Files pane so the active workspace gets the freed width. The engine or shell in the workspace remains visible. Toggle with ctrl+a z.

KeyTypeDefaultWhat it does
zen.activebooleanfalseOn/off. Persisted, so switching projects keeps you in zen

Zen always keeps the Tasks rail visible, because the rail also contains the exit affordance. A zen.keepTasks value left in your state.json by an older Rove is ignored; nothing reads or writes it any more.

Worktree location

By default new worktrees land under ~/.rove/worktrees/<repo-key>/<slug>.

KeyTypeDefaultWhat it does
worktree.basePathstring~/.rove/worktreesWhere new worktrees go
worktree.basePath.customstringunsetRemembers your last custom path in the TUI
worktree.cloneIgnoredbooleantrueClone ignored directories into new worktrees; see below

worktree.basePath takes an absolute path, or one starting with the $project_dir token, which expands to each task's project root, so one setting that gives you a per-project layout. $project_dir/.. puts worktrees next to each repo. The token only counts as the first segment.

Only new tasks move. Existing tasks keep the path they were created with, including legacy global and repo-local roots. worktree.basePath is local-only: remote (SSH) worktrees go under the remote project's own path at <project>/.rove/worktrees, and their existing .rove/worktrees remain discoverable. No restart needed.

Cloned ignored directories

A new local task's worktree starts with no node_modules, .venv, target or .build, because git does not check out ignored files. On macOS Rove clones those directories from the project's main checkout into the new worktree before .rove/init.sh runs, using APFS copy-on-write (clonefileat(2)). The copies cost almost no disk until a file changes, so bun test and friends work at once. init.sh still runs afterwards and stays authoritative: a lockfile that differs from the main checkout is its job to reconcile.

SettingWhereDefaultWhat it does
worktree.cloneIgnoredstate.jsontruefalse turns cloning off for every project. No restart needed
.rove/clone-dirsin the repothe four names aboveDirectory names to clone, one per line, # comments. A non-blank file replaces the defaults, so a comment-only file clones nothing for that repo. Wins over the defaults the way init.sh wins over the state.json override

Names are matched anywhere in the tree, so node_modules also picks up packages/*/node_modules. Entries containing a /, exactly . or .., or starting with -, are ignored.

A directory is cloned only when all of these hold; otherwise it is skipped without a message, exactly as before:

  • the project is local (not SSH) and the task owns a worktree (project-main and directory tasks do not);
  • git reports the directory as ignored and containing no tracked file;
  • it is absent from the new worktree;
  • source and worktree are on the same APFS volume (st_dev match).

A clone that fails (no /usr/bin/perl, say) is logged to the daemon log; it never fails task creation and leaves no partial directory. Each directory is one clonefileat(2) call, run concurrently, asynchronously: on a 2 GB, 87,000-file node_modules plus six nested ones, task creation went from 0.8 s to about 2.5 s. du counts clones at full size, so measure with df: that clone cost about 35 MB of free space.

Deleting a fresh task needs no --force: the four default names, when this worktree clones them, do not count as gitignored work. A name you add to .rove/clone-dirs is cloned but still counts, so a data directory is never silently deletable.

The current tree sidebar follows persisted project/task order and supports manual project reordering with shift+m. The t key cycles the task sort through four orders — the persisted one, most-recently-touched, attention (tasks blocked on you first, then ones whose turn landed unread, most-recently-touched inside each group), and name (A→Z by title, ignoring case, with numbers in numeric order so task 2 comes before task 10). Projects keep their own order in every mode. The choice is saved as activeSortMode and read back on startup; a value this build does not recognise reads as the persisted order. Older state files may contain tasksPane.projectFilter; the daemon still mirrors that compatibility value for background consumers, but the current PureTUI tree does not consume it.

Machines

machines maps a local alias to another computer running Rove. Written by rove machine add; nothing here needs hand-editing.

FieldWhat
hostSSH host as typed — usually an ssh_config Host alias, not a DNS name
userLogin user, or absent to let ssh_config decide
portSSH port, or absent for the ssh_config default
auth{"kind":"key"} (agent / default identities), {"kind":"key","keyPath":"…"}, or {"kind":"password","keychainRef":{…}} — a password is never stored here, only a pointer to it
identity{hostname, homeDir, daemonPid} learned from the machine's last handshake. Two aliases whose triples match are one machine
sockets{daemon, pty} remote socket paths the machine last reported, cached so a reconnect skips a second SSH round-trip. Absent until the first successful connect; never derived locally
addedAtISO timestamp of registration

Experimental

Off by default. These can change without notice.

KeyWhat it enables
experimental.remoteProjectsLets rove add --remote register a NEW project over SSH. Gates that one command only: remote projects already registered keep working — worktree routing and engine launch never read the flag — so turning it off does not disable them
experimental.autoStatusTasks move to in_progress and self-report in_review
experimental.dispatcherPer-repo routing of field notes between sessions

Themes

Rove bundles four themes (claude, conductor, skylight, and tokyonight) and ten more are one command away. skylight is built around its light half: set themeMode to light or auto to see it.

rove theme list
rove theme add https://rove.run/themes/gruvbox.json
rove theme remove gruvbox

Available hosted: catppuccin, dracula, everforest, gruvbox, kanagawa, nord, opencode, osaka-jade, rose-pine, solarized. Preview them at https://rove.run/themes.

You can also drop your own <name>.json into ~/.rove/themes/. No recompile, loaded at boot, and a user theme wins over a bundled one with the same name. Writing one: Themes.

Keybindings

Full vocabulary: Keybindings. The configuration surface:

  • Edit ~/.rove/settings/keybindings.yaml by hand (.yml is accepted when .yaml is absent). Rove never writes it.
  • Changes reload live, no restart. Problems show up as warnings in Settings → Keybindings.
  • A direct override replaces that binding's whole chord list; null or [] unbinds it. Prefix overrides set second-stroke keys and keep the original pane scope. Platform overlays (darwin:, …) win per chord.
  • A plugins: section binds chords to installed plugin panes and actions. Rove ships no default plugin chords.
  • Unknown ids are ignored with a warning; a typo never breaks the keymap.

Notifications and sound

Three kinds: done (green), needs_input (yellow), error (red). Yellow and red outrank green when both fire for the same tab. Three delivery channels:

  • Toasts. In-TUI, 4.5 seconds. Error toasts always show, even with toasts disabled: a failure shouldn't vanish because you turned off completion popups.

  • Desktop notification. Rove emits an OSC 9 escape that iTerm2, kitty, WezTerm, and Ghostty turn into a real OS notification; other terminals ignore it. Because it travels down the terminal stream, it reaches you over SSH. No separate switch — it rides the same notifications.sound.enabled toggle as the chime.

  • Sound. A short chime when a background tab finishes. Rove uses the first player it finds on PATH (ffplay, mpv, mpg123, … afplay, play, aplay, …), and on Windows falls back to PowerShell. With none installed it's silent and the terminal bell is the fallback.

    Volume is a property of the audio Rove hands the player, not a flag it passes: several players (afplay, aplay, and the Windows fallback, whose Media.SoundPlayer has no volume API) accept no volume argument at all, so Rove scales the chime's samples itself and caches one copy per level. Cycle it from Settings → General → Chime volume, or set notifications.sound.volume directly; 0 is silent.

Custom engines

Built-in engines are claude, codex, copilot, kimi, pi, and omp. You can register any other CLI from Settings → Engines, or by hand:

{
  "customEngineIds": ["aider"],
  "engineCommand.aider": "aider --model sonnet",
  "engineName.aider": "Aider"
}

Switching an engine OFF in Settings → Engines (space) records it under disabledEngineIds; it keeps every override and simply stops being offered when you pick an engine for a task. That covers the headless path too: a disabled engine is skipped by rove api add's repo default, so switching one off after using it in a project does not leave that project still launching it. The global default engine can't be left disabled; switching it off hands the default to the first engine still on.

Being in customEngineIds is the registration. There's no other step. Settings → Engines rejects a blank id, one that shadows a built-in, and one already registered; it lowercases and trims what you type and accepts the rest. Keep ids to lowercase letters, digits, - and _: the id becomes both a --command <id> argument and a key in state.json, so a space or a quote in one makes it awkward to pass and awkward to hand-edit.

A custom engine launches and runs like any other, but Rove deliberately doesn't guess at its internals — no history reader, no account detection, no activity hooks, no session resume — unless you declare "engineProtocol.<id>" (one of the built-in ids: claude, codex, copilot, kimi, pi, omp), which borrows that built-in's adapter for transcript reads and delivery. More in Engines.

Settings → Engines asks for it while adding the engine — a list of the built-ins plus None, so the generic adapter is something you choose rather than something a typo leaves you with — and prints the answer under the engine's row afterwards. Changing it means removing the engine (x) and adding it again, or editing the key here by hand.

Claude Code plugin

Rove integrates with Claude Code through global activity hooks (so the sidebar can show working / done / needs-input for every session) and the companion agent skill. There are two ways to get both, and you should run exactly one:

  • Default (no action needed): every Rove launch idempotently writes its hooks into ~/.claude/settings.json, and rove skill install places the skill. If CLAUDE_CONFIG_DIR is set to a nonblank path, hooks instead go into <CLAUDE_CONFIG_DIR>/settings.json. This is what most existing installs use.

  • The Claude Code plugin. One install carries hooks and skill together, with no PATH or settings.json involvement:

    /plugin marketplace add Sma1lboy/rove
    /plugin install rove@rove

    The plugin's hook commands call a bundled wrapper by absolute path, so they work even when rove isn't on the shell's PATH. The bundled skill is versioned with the plugin rather than with Rove itself (/plugin update refreshes both), so Rove's skill staleness prompts step aside while the plugin is enabled.

Once Rove sees the plugin enabled, it stops writing the Claude hooks into settings.json on launch. If you were running Rove before installing the plugin, the old settings-managed hooks are still there and every event would fire twice. Rove warns about this at startup and the fix is one command:

rove hook cleanup

That removes Rove's entries from the active profile's settings.json; your other hooks, including commands in the same group, are preserved. Installation and cleanup leave invalid or unreadable settings unchanged. They also refuse non-regular files and files over 8 MiB. JSON rewrites use owner-only read/write permissions (0600). Startup cleanup of retired global hooks uses this same profile; explicitly saved repository or settings-file cleanup paths still apply. If you also have a pre-plugin skill copy under ~/.claude/skills/rove (or …/rove), delete that directory. The plugin's bundled copy replaces it. Rove never edits or removes either one silently.

Uninstalling or disabling the plugin reverses the handoff: the next Rove launch reinstalls the settings-managed hooks automatically.

Only Claude Code is affected. Codex (and other engines') hook integration is engine-owned and unchanged by the plugin.

Per-repo init

A repo can ship two files in its own .rove/ directory:

  • .rove/init.sh. Runs in each new task worktree before the engine starts, once per worktree. Use it for bun install, direnv, codegen.
  • .rove/init-prompt.md. Sent as the engine's first message.

init.sh comes from the repo, and Rove runs it without asking — in the same shell that then execs the engine, with your account's privileges. That is the same level of trust you extend by running the repo's own build or test command, so it is worth a look at .rove/init.sh before you create the first task in a repository you did not write.

Files under .rove/ win over per-user overrides set with rove repo set. For init.sh, the file only has to exist, even if empty. Prompt files must be non-empty. The retired repository directory is no longer read.

The PR action also reads .rove/pr-instructions.md as its prompt template; {{branch}}, {{targetBranch}}, {{dirtyCountSentence}}, and {{upstreamSentence}} are substituted (unknown {{…}} passes through). The file must be non-empty.

On this page