Throwaway probe: does userConfig give a plugin /config rows, a picker, values at register(), and what does a change cost?

Vim-style marks, a reading position, and a numbered prompt history, inside a Claude Code conversation
A Claude Code plugin that lets you mark a line of a conversation with a letter and jump back to it from anywhere, keep your place while you scroll, browse every prompt you have typed, and let Claude cite earlier places as links you can click. It is a mod, meaning it draws inside Claude Code's terminal app, with nothing else to install. In Claude Code's official directory the plugin is called bookmarks (published by DazzleML), whereas the Github repository here is claude-bookmarks. (It is a terminal plugin, not a browser extension -- for navigating the claude.ai website, AI Chat Nav does a similar job).
A long Claude Code conversation is hard to move around in. The answer you need is three hundred messages up, the prompt that started this line of work is somewhere above that, and after you scroll up to check something, finding your way back down to where you were reading is an exercise in scrolling-squinting-and-praying. Claude Code can already jump to the top and the bottom of the conversation, and its transcript view can search, but it has no way to say "remember this spot" and come back to it.
claude-bookmarks gives you named spots in the conversation. This includes tagged letters you set on any line (essentially Vim marks), a reading position you can swap to and back from, and a numbered list of every prompt you have typed, so "go back to where I asked about the cache" is two keystrokes instead of a text safari.
[!NOTE] Pre-alpha (v0.3.x).
claude-bookmarksis part of my daily active workflow. I've been dogfooding since Claude Code 2.1.288 (now at 2.1.295) on Win11 using Windows Terminal, fullscreen, and at different term-sizes. The mod for the most part correctly handles: inserting in-place highlights, jumps, using the reading position, navigating the prompts pane, and now works with bookmarks including clickable links in Claude's replies. Windows is the only tested platform. MacOS and Linux are expected to work but is untried (I'll try on a VPS after a few more minor versions). The engine underhooks/core/is tested and reasonably solid. Be aware the key layout will still change (#26). Also note: docs/status.md tracks known issues along with what is coming in the Roadmap and issue #1 (the longer view). Please file issues for anything rough.
<img src="plugin/images/prompts-pane-marked-line-and-bookmark-link.png" alt="The prompts pane open on the right, numbered from the first prompt; a marked line in the conversation with its c tag; a bookmark link in a reply; the band above the prompt in prompt mode"> <sub>In use: the prompts pane (<code>Ctrl+] p</code>) on the right, a marked line with its <code>«c»</code> tag, a <code>⚓</code> bookmark link Claude wrote in a reply, and the band above the input box waiting for a number. <a href="docs/images/jump-pane-marks-and-reading-position.png">The jump pane</a>, with three marks and the reading position.</sub>
One command installs it on Claude Code 2.1.287 or later, since this repository is also a marketplace:
/plugin install bookmarks --marketplace DazzleML/claude-bookmarks
To work on it, or to run a version before it was listed, load a local clone:
# 1. Clone the plugin
git clone https://github.com/DazzleML/claude-bookmarks.git
# 2. Load it (the plugin is the repository's plugin/ folder). For one session:
claude --plugin-dir /path/to/claude-bookmarks/plugin
# ...or for every session, add it to ~/.claude/settings.json:
# "env": { "CLAUDE_CODE_PLUGIN_DIRS": "/path/to/claude-bookmarks/plugin" }
# 3. Bind the leader key in ~/.claude/keybindings.json (see "Set up the leader" below)
# 4. Restart Claude Code, and switch to the fullscreen renderer if you are not on it
/tui fullscreen
# 5. Check it loaded
/bm-env
When the plugin is loaded, a one-line band sits above the prompt: a small bm: field, then four buttons, mark jump prompts read.
The plugin's keys all start with one leader key, Ctrl+]: a single key that every terminal passes on, over SSH as well. It is Claude Code's own "focus the band" action, abovePrompt:focus, bound to that key. A plugin cannot add a keybinding for you, so add this once to your ~/.claude/keybindings.json (back the file up first; if the file already has a Chat block, add the one line to it):
{
"bindings": [
{
"context": "Chat",
"bindings": {
"ctrl+]": "abovePrompt:focus"
}
}
]
}
That's the whole setup. Every key in these docs is written as Ctrl+]. Until the binding is in place, Claude Code's built-in key for the same action, Ctrl+X Tab (two keys), does the same job, and the band's buttons can be clicked.
[!TIP] New to vim-style keys? Start with the tutorial: it explains chords, leader keys and vim marks in plain terms, then walks you through each feature in about five minutes.
Ctrl+] m and a letter (a-z). With nothing freshly selected, the letter marks the message at the top of your screenCtrl+] ' and a letter scrolls back to that mark from anywhere in the conversation; the jump pane lists only the marks you haveCtrl+] Space Space to keep your place; press it again from anywhere to go there, and again to return to where you wereCtrl+] p lists every prompt of the conversation, numbered from your first, including prompts from before the plugin was loaded; type a number, or browse with j/k or the arrows, then Enter to jumpCtrl+] b); promote any mark into a bookmark with Ctrl+] P and its letterCtrl+] o returns to where you were before the last jump, Ctrl+] i goes forward again, like vim's jumplist--resumesh and grep elsewhere)| Keys | Band button | What it does |
|---|---|---|
Ctrl+] m then a-z | mark | Mark the selected line (or the top of the screen) with that letter; Enter cancels |
Ctrl+] ' (or j) then a-z | jump | Jump to that mark |
Ctrl+] ' then Enter | jump | Jump to the reading position (it heads the jump pane) |
Ctrl+] Space Space | read | Set, go to, or return from the reading position |
Ctrl+] p then a number, Enter | prompts | Jump to prompt #N |
Ctrl+] p then j/k or Down/Up, Enter | prompts | Browse the prompts one by one, then jump |
Ctrl+] p, pick a prompt, then s | prompts | Pin or unpin it (the band stays open to pin more) |
Ctrl+] b then a number, Enter | Open the bookmarks pane (yours, then Claude's) and jump to bookmark #N | |
Ctrl+] P then a-z | Promote that mark into a bookmark on your list (its link is copied to the clipboard) | |
Ctrl+] o / Ctrl+] i | Back to before the last jump / forward again | |
Click a ⚓ link in a reply | Jump to the bookmarked message, words highlighted; ctrl-click opens its file | |
Esc | Leave the band and go back to typing |
After a command the keyboard stays on the band, ready for the next one; Esc returns to the input box. In the mark pane, letters already in use show as ●. Prompts drawn dimmed in the prompts pane are ones Claude Code hasn't drawn on screen, usually from before the last compaction, and a jump to one may be refused (see Tips).
Power users can add one-step keys that press a band button directly, such as Ctrl+X Space for the reading position; they borrow actions of Claude Code's diff panel, so they aren't part of the default setup. See Optional fast keys.
# Keep an answer you will need again
select a line of it -> Ctrl+] m a (mark a)
...later, anywhere -> Ctrl+] ' a (back to it)
# Check something above, then come back
select the line you are reading -> Ctrl+] Space Space (reading position set)
scroll up, read what you needed
Ctrl+] Space Space (back to the reading position)
Ctrl+] Space Space again (and back to where you were)
# Go back to where a line of work started
Ctrl+] p 12 Enter (prompt #12)
# Ask Claude where to read, and click your way there
"Where should I read to catch up? Bookmark each place." (Claude answers with ⚓ links)
click a link (jump; the words light up)
Ctrl+] o (back to the answer)
# Keep a mark for good
Ctrl+] P a (mark a becomes a bookmark; link on the clipboard)
| Command | What it does | |||
|---|---|---|---|---|
/bm-mark, /bm-goto, /bm-prompts, /bm-read | The same as the leader keys, typed; from an empty input box their pane takes the keyboard | |||
/bm-bookmarks, /bm-promote a | Open the bookmarks pane; promote mark a into a bookmark | |||
/bm-delmarks a b, /bm-delmarks all | Delete marks | |||
/bm-pin [N] | Pin or unpin prompt #N in the prompts pane (*N in its # field does the same) | |||
/bm-env | Plugin version, session id, and what it has captured | |||
| `/bm-debug on\ | off` | Echo the plugin's log into this conversation as dim rows (this conversation only; `default on\ | off and force on\ | off` reach every conversation) |
| `/bm-debug enable\ | disable <channel>` | Turn a heavier diagnostic on or off for this conversation (screen, held; /bm-debug channels lists them) | ||
/bm-marks | The marks set in this conversation | |||
/bm-timeline | The last few plugin events (draws, panes, jumps) |
For each feature in detail, see docs/usage.md.
The known limits, briefly; docs/troubleshooting.md has the details.
Esc to go back to typing. After a command the keyboard stays on the band; a plugin can't hand it back to the input box itself.Ctrl+O, then (writes what Claude Code holds to your terminal's scrollback), then your terminal's Find (Ctrl+Shift+F or Cmd+F), and paste. Older messages may only be in the session file; opening it in full is planned ([#16). For long conversations there is a way to keep the whole history in view after a restart, described in the engine notes; the plugin never depends on it./fork, the session may be hosted by Claude Code's background daemon, which doesn't load CLAUDE_CODE_PLUGIN_DIRS. Stop it with claude stop <short id> and resume it from a shell with claude --resume <session id>.Ctrl+X fast keys and have disabled Claude Code's built-in diff mod, or have the diff panel open, the diff panel may take those keys.Open /config and find the plugin's settings under its name. A change takes effect at once; /bm-config prints every setting, its value and where the value came from.
| Setting | Values (default first) | What it does |
|---|---|---|
| Prompts list opens at | nearest, newest | Where the prompts list opens: centred on the prompt nearest your view, or at the newest |
| Prompts list follows the view | when-idle, always, never | Whether the list scrolls to keep the yellow row showing as you scroll the conversation (when-idle: not while you type a number) |
| Closeness for the earlier prompt | 0.6 (0 to 1) | How much of the screen an earlier exchange must hold to count as nearest |
| Footnotes in the prompts list | folded, shown, hidden | Your slash commands, ! shell commands and interruptions, listed as N.k under the prompt they followed: folded behind … N footnotes, always listed, or never |
| Prompt text kept for the filter (bytes) | 2000000 (0 = unlimited) | How much of your prompts the /words filter searches, newest first; past the budget the oldest prompts are matched only on the short excerpt the list shows |
| Prompts or bookmarks list after you pick a row | close, stay | Whether the list closes after you click a row (or type its number and Enter), or stays open for the next pick |
| A list while you type | stay, close | Whether an open list stays while you type in the input box |
| The band | full, field | The command field with the labelled commands and ?, or the field alone |
| Chords on their own keybindings | on, off | Whether mark, jump, prompts and read also answer the optional chords bound to their borrowed actions |
| Lists in a pane | on, off | Whether the prompts and bookmarks lists open in a pane, or p/b take a number in the band with no pane |
| Keyboard after an action | input, band | Where the keyboard should go after an action (input waits on Claude Code allowing it) |
| Debug echo by default | off, on | Whether the debug log is echoed into conversations that have not chosen (/bm-debug decides per conversation) |
| Conversation text in the debug log | auto, off, with-debug, always | Whether debug lines quote the conversation; auto is off in an installed plugin, always in one loaded from a folder |
| How much text a debug line quotes | reference, full | A short excerpt to find the place, or the whole text |
| Colour overrides | JSON, empty by default | A role's colour, background, dim or bold, e.g. {"here": {"color": "cyan"}}; roles: here, pinned, footnote, legend, mark, markBand, used, unused, unreached, emphasis, search; a colour is a theme key (warning, subtle, ...), a terminal colour name or #rrggbb |
Still constants, planned as settings (#7): how old a selection may be before a mark uses the top of the screen (75 seconds), where "back" from the bottom returns, whether a typed prompt number waits for Enter, the highlight's duration, and the pane sizes.
Every key press, pane and jump is written to a per-conversation log, kept to its last 400 lines:
${CLAUDE_USER_DIR:-~/claude}/bookmarks/debug/<session id>.log
Read its last lines first when something behaves oddly; they make a good attachment to an issue.
| Platform | Status |
|---|---|
| Windows 11, Windows Terminal, fullscreen | Tested |
| macOS, fullscreen | Expected to work |
| Linux, fullscreen | Expected to work |
| Classic (non-fullscreen) renderer | Not supported |
Details, including tmux, IDE terminals and the Desktop app: docs/platform-support.md.
claude-bookmarks/
├── plugin/ # The plugin, as Claude Code installs it (point --plugin-dir here)
│ ├── .claude-plugin/
│ │ └── plugin.json # Plugin manifest (name: bookmarks)
│ ├── hooks/
│ │ ├── hooks.json # Points Claude Code at the mod
│ │ ├── register.tsx # The mod: the band, panes, highlight, bookmarks, back-fill
│ │ ├── core/ # Pure modules: anchor URL, transcript, register file, keys, view position
│ │ └── scripts/ # PowerShell fallbacks for reading the transcript on Windows
│ ├── types/index.d.ts # Declared $.state values
│ ├── README.md, LICENSE # What the plugin directory requires inside the plugin folder
│ └── tsconfig.json # Type-check the mod: npx tsc -p plugin --noEmit
├── .claude-plugin/
│ └── marketplace.json # This repository as a marketplace (dazzle-claude-plugins)
├── docs/ # Status, keys, tutorial, usage, troubleshooting, platform support, engine quirks
├── scripts/repokit-common/ # Shared repo tooling (git subtree from DazzleTools/git-repokit-common)
├── tests/
│ ├── core/ # node --test suites for the pure modules (kept out of the installed plugin)
│ ├── checklists/ # Human test checklists
│ └── one-offs/ # Probes and measurements
├── .repokit-common.toml # Repo tooling settings (version sync into plugin/.claude-plugin/plugin.json)
└── version.py # Version source for the repo tooling
abovePrompt:focus) moves the keyboard to the band, where a small field takes the next key, any key, ' and Space included. The band then shows what it waits for and takes the rest of the keys itself, while a pane beside the conversation shows the list. Claude Code credits a plugin's scroll or pane to your keystroke only while the key's handler is still running, so every jump happens inside that handler. The band exists because Claude Code refuses a pane the keyboard while the input box holds text; #26 is the plan to land in the pane whenever it is allowed.Select-String on Windows, grep elsewhere), then keeps the list current as you type.Contributions welcome! Please open an issue or submit a pull request.
See CONTRIBUTING.md for:
claude --plugin-dir . reloads the mod live when you save)claude plugin validate . and python -m pytest tests/ -vsync-versions.pytests/checklists/Like the project?
claude-bookmarks, copyright (C) 2026 Dustin Darcy
This project is licensed under the GNU General Public License v3.0 or later - see the LICENSE file for details.
hooks/register.tsx 65 lines1// Settings spike for claude-bookmarks (goal slice 1, 2026-10-11). A throwaway probe in a
2// scratch plugin named bookmarks-spike, so it cannot disturb the live plugin or its store.
3//
4// It answers: does /config list this plugin's userConfig fields and draw the picker for
5// a declared `options` list; do the values reach register(on, options); does a change in
6// /config reload the module (LOADED_AT changes when it does); can $.config.set run from a
7// typed command and from a pane button, and does that reload too.
8//
9// /bm-spike log the module's load time, the options it was given, and the
10// plugin's /config rows
11// /bm-spike set followHere always set a field from a typed command
12// /bm-spike pane open a pane with two buttons that set fields
13import type { Register } from 'claude-code'
14
15const LOADED_AT = new Date().toISOString()
16const PANE = 'bm-spike'
17const PREFIX = 'bookmarks-spike.'
18
19export const register: Register = (on, options) => {
20 on('session.start', async ($, e, next) => {
21 await $.command.register({ name: 'bm-spike', description: 'Settings spike: show options and config rows (set <field> <value> | pane)', immediate: true })
22 $.ui.log(`[spike] module loaded ${LOADED_AT}; options = ${JSON.stringify(options ?? null)}`)
23 return next(e)
24 })
25
26 on('command.run', { command: 'bm-spike' }, async ($, e) => {
27 const arg = e.args.trim()
28 const rows = (await $.config.list()).filter(r => r.key.startsWith(PREFIX))
29 $.ui.log(`[spike] loaded ${LOADED_AT}; options = ${JSON.stringify(options ?? null)}; rows = ${JSON.stringify(rows)}`)
30 if (arg.startsWith('set ')) {
31 const [, field, raw] = arg.split(/\s+/)
32 const value = raw === 'true' ? true : raw === 'false' ? false : raw ?? ''
33 try {
34 const r = await $.config.set({ key: PREFIX + field, value })
35 $.ui.log(`[spike] set ${field}=${String(value)} from a command -> ${JSON.stringify(r)}`)
36 } catch (err) {
37 $.ui.log(`[spike] set ${field} from a command threw: ${String(err)}`)
38 }
39 }
40 if (arg === 'pane') await $.ui.open({ id: PANE, title: 'settings spike', focus: true, closeOnEscape: true, columns: 16, rows: 4 })
41 return {}
42 })
43
44 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
45 const { Box, Button, Text } = $.ui.resolve(e)
46 const press = (field: string, value: string | boolean) => async () => {
47 try {
48 const r = await $.config.set({ key: PREFIX + field, value })
49 $.ui.log(`[spike] set ${field}=${String(value)} from a pane button -> ${JSON.stringify(r)}`)
50 } catch (err) {
51 $.ui.log(`[spike] set ${field} from a pane button threw: ${String(err)}`)
52 }
53 }
54 return (
55 <Box flexDirection="column">
56 <Text dimColor>module loaded {LOADED_AT}</Text>
57 <Text dimColor>options {JSON.stringify(options ?? null)}</Text>
58 <Button key="a" label="followHere = always" onPress={press('followHere', 'always')} />
59 <Button key="b" label="followHere = never" onPress={press('followHere', 'never')} />
60 <Button key="c" label="paneAfterJump = true" onPress={press('paneAfterJump', true)} />
61 </Box>
62 )
63 })
64}
65