notenerd3k downloadsA real terminal with shell integration and beginner-friendly command help, plus a multi-agent workflow for Claude Code sessions: word-level diffs, undo, backlink-breakage warnings, and more.
⭐ Multi-agent is now the default. Run several Claude sessions side by side with an Agent inbox and groups for your terminals. Prefer the single Pending Changes panel? Open Settings → Terminus → Experience and choose Classic (or click Switch to Classic in the "What's new" popup shown once after you update). How to switch · See what it does.
A real terminal inside Obsidian that never lets you lose the thread on what Claude Code just did. You type claude (or anything else) exactly like you would in any other terminal, with nothing wrapped or intercepted, and Claude keeps working through a whole multi-file turn uninterrupted. Two things happen because of that:
Edits land on disk immediately; you review what changed afterwards, with the option to revert.
Terminus opens in the Multi-agent experience: an Agent inbox with a card per Claude session, and groups to keep related terminals together. Prefer one Pending Changes panel? You can switch to Classic any time. See Choose your experience.
Real Claude sessions running side by side. Groups on the left show who is working and who needs you; the Agent inbox lists what each changed, down to its subagents. When a session needs permission, Next jumps straight to it.
Terminus has two experiences. They share the same terminal, the same hooks and the same pending changes; they differ in how you keep track of Claude's work.
| Classic | Multi-agent | |
|---|---|---|
| Best for | One Claude session at a time, or a few you watch yourself | Several Claude sessions running side by side |
| Where you review | One Pending Changes panel, grouped by terminal | An Agent inbox with a card per session, and groups for your terminals |
| Organising terminals | Tabs, names and colours | Groups, snooze and settle, a "go to" picker, forking and sending tasks between sessions |
| Talking to a running session | Type in its terminal | A steer / queue box on each card |
| Default | Opt in | Yes (new installs and updates) |
To switch: open Settings → Terminus → Experience and pick Classic or Multi-agent. Switching is instant and reversible: terminals, pending changes and every other setting carry over, and each experience keeps its own review settings.
Updating from an earlier version? If you hadn't chosen an experience yet, Terminus moves you to Multi-agent once and shows a "What's new" popup with a short demo, a Read the guide button (opens the in-app guide at the Multi-agent section) and a Switch to Classic button. If you had already declined Multi-agent or picked Classic yourself, nothing changes.

claude.Cmd/Ctrl + click a file path or link in terminal output to open it. Files in your vault open in a new tab — notes as notes, and anything else in whichever editor has claimed that extension — with a trailing :42 (the form compilers, linters and Claude all print) landing on that line; URLs open in your browser; anything Obsidian can't open at all, including dot-files and paths outside the vault, goes to your system's default app. Paths are checked against disk before being linkified, so prose and version numbers don't turn into false links.[[ in a terminal to fuzzy-search your vault's notes and insert one as a wiki-link, a vault-relative path, or an absolute path, configurable in Settings.claude) automatically in every new terminal once its shell is ready..zshrc/.bashrc.claude -p call with no file/tool access. Its result has a fixed spot above the action buttons and stays there regardless of what else you do in the popup. Both this and Suggest a fix see not just the failed command but a few commands run right before it too, so a failure that's really about an earlier step (e.g. a git push failing because nothing was committed yet) gets diagnosed with that context, not just the last line.| Command help: Explain / Suggest a fix |
|---|
![]() |
| Rename a terminal |
|---|
![]() |
| Color-code a terminal, with the tint carried to its native tab |
|---|
![]() |
The single-panel experience. Every file Claude edits lands in one Pending Changes panel, grouped by terminal, where you accept or reject it after the fact. Edits land on disk immediately and nothing is blocked. (Multi-agent reviews through the same hooks and the same diff views; it just presents them per session in the inbox.)
| Split Diff: per-hunk Accept/Reject |
|---|
![]() |
PreToolUse hook records the pre-edit snapshot of every file Claude touches — through Edit/Write/NotebookEdit or through a shell command — but never blocks the write. Claude finishes its whole turn; you review afterward.Stop hook tells the plugin when the turn actually ends. The dot carries that terminal's own color, so it's clear which session is busy.[[Note#Heading]] or [[Note#^block]]..ts, say) opens in that editor without the inline markup rather than being swapped for another view, and files Obsidian can't open at all — dot-files, paths outside the vault — go to your system's default app, the same as a Cmd-clicked path in the terminal.NotebookEdit changes, since their diff is a display-only approximation of the real file content and can't be safely spliced.Multi-agent is the default: it lets you run several Claude sessions side by side without losing track of them. If you would rather have Classic's single Pending Changes panel, switch back in Settings → Terminus → Experience. The two share your terminals, pending changes and every other setting, and each keeps its own review settings.

Groups on the left, and the Agent inbox on the right following the selected group. The working session's changes hang under the lead and each subagent that made them.
| Steer a running session, or queue messages for when its turn ends |
|---|
![]() |
| Reorder, edit and steer queued messages |
|---|
![]() |
| Fork a session into a new terminal |
|---|
![]() |
| Send a task to a new terminal |
|---|
![]() |
| Snooze a group until later |
|---|
![]() |
| Review a change hunk by hunk from the inbox |
|---|
![]() |
status. When a turn ends with the note active, Terminus suggests moving it to review (and back); it never changes the note on its own.cat > note.md <<'EOF' as readily as for the Write tool, so the shell is a first-class path, not an afterthought. A Bash hook pair reads the files a command looks like it will write before it runs and again after, and whatever really changed is recorded as an ordinary row — same diff, same Split Diff, same Keep/Reject. A path it guesses wrong costs nothing: the content matches, so no row appears.git status across each turn to catch those. One that was clean when the turn began has an exact before-state, its content at HEAD, so it becomes a normal reviewable row too. The rest — a rename, binary content, a file that already had uncommitted changes — are listed by name and status with no diff, because there is nothing exact to revert to and a review list that is silently empty reads as "nothing happened". Turn it off under Settings → Detect shell changes.claude --resume <id> --fork-session), named "(fork)".Off unless you turn it on. Settings → Terminus → Log sessions to notes makes Terminus write one note per Claude session into your vault (Terminus/Sessions, or wherever you point it), appending a section as each turn ends.
.base file — change it however you like, and if you delete it, it stays deleted; point the folder setting at a new folder and that one gets a view of its own. Needs Obsidian 1.9.10 or newer, where Bases is a core plugin; below that the notes are still written, just without the view.It ships off because it is the only part of Terminus that writes your prompts to disk — the action log deliberately keeps filenames and statistics and nothing else. Prompts are recorded up to 2,000 characters.
All of these appear in the command palette under Terminus:. None ship with a default hotkey — Obsidian's plugin guidelines advise against claiming key combinations on a user's behalf — so assign your own in Settings → Hotkeys for any you use often.
| Command |
|---|
| Open |
| Open in new tab |
| Open in split right |
| Open in split down |
| Open in new window |
| Open Pending Changes |
| Open agent inbox |
| Jump to next session that needs attention |
| Open Action Log |
| Getting started |
| Rescue closed terminal |
| Increase terminal font size |
| Decrease terminal font size |
| Reset terminal font size |
| Accept oldest pending change |
| Reject oldest pending change |
| Keep all pending changes |
| Reject all pending changes |
| New group (Multi-agent) |
| Switch group… (Multi-agent) |
| Go to terminal or group… (Multi-agent) |
| Move tab to group… (Multi-agent) |
| Close group (Multi-agent) |
| Start Claude on this note (Multi-agent) |
| Send a task to a new terminal… (Multi-agent) |
Review workflow. The plugin auto-provisions a project-scoped .claude/settings.local.json in your vault wiring a PreToolUse hook (matching Edit|Write|NotebookEdit, and separately Bash) to a bundled script. That script POSTs the tool call to a local, loopback-only HTTP server the plugin runs, which records the file's current content (before the write happens) and lets the hook return immediately, so Claude is never blocked. Rejecting a change later writes that recorded content back to disk.
Knowing when a turn ends. The same file also gets a Stop hook, pointing at a second bundled script that pings the same server when Claude finishes a turn. Nothing in the PreToolUse stream can express "no more writes are coming" — a turn goes quiet whenever Claude is thinking or running non-write tools — so without it the activity chip has to guess with a timer and either drops mid-turn or lingers past the end. It's a bodyless ping that writes nothing to stdout, since a Stop hook's stdout is how a hook tells Claude Code to block stopping.
Knowing when Claude needs you. A third hook, Notification, points at a third bundled script and fires whenever Claude Code needs permission to use a tool or has sat idle waiting on input. Both are "a human needs to look here" moments, so either one lights up that terminal's tab dot the same way, without the plugin parsing the notification's message text. The Stop hook (above) or focusing that tab, whichever comes first, clears it again.
Multi-agent. Both experiences also get SessionStart and UserPromptSubmit, which shell-change detection needs to know when a session restarts and when a turn begins. The Multi-agent experience adds hooks for SessionEnd and StopFailure (and, while subagents are tracked, the Agent tool's PreToolUse, SubagentStart and SubagentStop) to the same bundled bridge scripts. Every event carries its terminal's own token, so a session, its subagents and their edits are always attributed to the right terminal. Nothing polls. If your vault path contains a space, the hook commands are written single-quoted so the shell runs them correctly.
Tying a change to the request behind it. Every hook payload carries a prompt_id, and — checked against Claude Code 2.1.304 — one id covers a whole user turn, across every API request inside it, while a subagent's calls carry their lead's id rather than one of their own. UserPromptSubmit and Stop carry the same id, so the prompt text, the edits it caused and the moment the turn ended all join up without inferring anything from event order. That is what the inbox groups by, and what each session-log entry is built from.
Noticing shell edits. A Bash call carries no diff of its own, so it is matched on both PreToolUse and PostToolUse: the first snapshots the files the command looks like it will write, the second re-reads them, and the difference is the change. Both carry the same tool_use_id, which is what pairs them. That covers >, >>, heredocs, tee, sed -i, cp, mv, touch and rm — anything naming its target. A heredoc's body is stripped before the command is parsed, so a note containing > or quotes is never read as shell syntax. For a file written indirectly, where no path appears at all, "Detect shell changes" reads git status as a turn begins and again as it ends and reports whatever changed in between that no hook and no snapshot accounted for. The baseline is per turn rather than per session because that is what keeps your own edits out of the result: yours happen between turns, whereas during one Claude is the only writer. It asks git for --show-cdup rather than --show-toplevel, so a vault reached through a symlink is spelled the same way the hook payloads spell it. Outside a git repository it does nothing at all.
All three hooks are added without disturbing anything already in that file, and Claude Code reads hooks at session start — so a claude that was already running when the plugin first provisioned them picks them up on its next start.
Terminal. There's no native-addon PTY dependency (no node-pty, no Electron-ABI rebuild headaches). A small bundled Python script (resources/pty_helper.py) allocates a real pseudo-terminal via Python's stdlib and proxies bytes between the PTY and its own stdio; the plugin talks to it with a plain child_process.spawn. The terminal UI is xterm.js.
Shell integration. Two tiny rc scripts (resources/shell-integration/{zsh,bash}/) get sourced by redirecting ZDOTDIR (zsh) or HOME (bash) for the spawned shell only, never the plugin's own Python helper process, restoring the real value immediately and chain-loading your actual .zshrc/.bash_profile before adding invisible OSC 133 command-boundary markers and an OSC 7 current-directory marker around each prompt. Nothing on disk is modified.
Command help. "Explain this" and "Suggest a fix" are standalone claude -p calls with --allowedTools "" (no file or shell access, pure Q&A), independent of whatever's running in any terminal, so they work even if no terminal has claude open at all.
Wiki-link autocomplete. A real terminal has no local echo: everything on screen is echoed back by the remote shell's own readline, not rendered by the plugin. So typing [[ can't be reacted to after the fact; it's intercepted before either character ever reaches the shell, and a resolved link/path is written in its place only once you actually pick one.
pty_helper.py uses os.waitstatus_to_exitcode, added in 3.9) and the claude CLI installed and logged in. Both are auto-detected (including a login-shell PATH lookup, since Obsidian launched from Finder/Dock often inherits a minimal one); if detection ever picks the wrong binary, Settings has manual overrides for the shell and Python 3 paths.claude setup-token in a real terminal and paste the result into Settings → Advanced → "Claude Code auth token", then use the Test button next to it to confirm -- takes effect immediately, no restart needed.git is optional. It powers the "vs git HEAD" diff view, the branch shown on each inbox card, and shell-change detection. Without it — or outside a repository — those three simply don't appear, and nothing else changes.npm install
npm run dev # unminified build with inline sourcemaps
npm run build # typecheck + minified production build
npm test # typecheck the tests, bundle them, run them
The build bundles src/main.ts into main.js with esbuild, and regenerates styles.css from src/css/custom.css plus xterm.js's own stylesheet. Copy (or symlink) the plugin folder into <vault>/.obsidian/plugins/terminus/ and enable it in Settings → Community plugins.
npm test runs Node's built-in test runner over test/*.test.ts. No test framework and no new dependencies: scripts/buildTests.mjs bundles each file with esbuild (already a dev dependency), since Node can't load TypeScript and the plugin's modules import obsidian, which only exists inside Obsidian. That import is aliased to a small stub in test/helpers/, and test/tsconfig.json typechecks the tests, which the production build deliberately doesn't — a release shouldn't fail on a test's types.
They cover the logic that can be exercised without a running vault, which is where the bugs have actually been:
| File | What it pins down |
|---|---|
test/gitStatus.test.ts |
Shell-edit detection against a real git repo: paths with spaces, a rename's trailing source field, deletions, ignored files, and work already dirty before the turn |
test/inboxModel.test.ts |
Turn records, active time, and the two grouping rules worth getting right: a row touched by several turns belongs to the first, and a subagent's edits belong to the lead's turn |
test/sessionLog.test.ts |
The session log's lifecycle against an in-memory vault: one note per session, numbering past the 50-turn cap, /clear closing the old note, a restart continuing the same one, an interrupted turn surviving, and the abandoned sweep |
test/sessionLogFormat.test.ts |
The exact text a note is made of, including that a prompt can't break out of its blockquote, forge a turn heading, or inject frontmatter |
Anything that needs Obsidian itself — the inbox's rendering, the terminal, the hook bridges — is still verified by hand in a vault.
| Path | Role |
|---|---|
src/main.ts |
Plugin entry: lifecycle, commands, settings persistence |
src/settings.ts |
Settings tab + settings type/defaults |
src/views/TerminalView.ts |
The terminal panel: xterm.js, PTY wiring, failure badges, rename/color identity |
src/views/PendingChangesView.ts |
The Pending Changes sidebar panel |
src/views/AgentInboxView.ts, TerminusSidebarView.ts |
Multi-agent: the Agent inbox (right) and the groups sidebar (left) |
src/groups/ |
Group logic: controller, menus, naming, notes, snooze presets, tab visibility, fork command, the "Go to terminal or group" picker |
src/state/AgentSessionStore.ts, GroupStore.ts, InboxUiState.ts, DelegationStore.ts, inboxModel.ts |
Session status, groups, how the inbox is arranged, delegated tasks, and sorting sessions into cards |
src/views/DiffSplitView.ts |
The Split Diff view (one pending change, opened per-file) |
src/diff/buildDiffLines.ts, renderDiff.ts |
Line-structured, word-emphasized diff model + gutter/minimap rendering |
src/diff/hunks.ts |
Groups a diff into context runs and hunks with stable offsets, shared by the Split Diff renderer and per-hunk resolution |
src/diff/renderSplitDiff.ts |
Renders the Split Diff view's side-by-side body and per-hunk controls |
src/pty/PtyProcess.ts, shellDetect.ts, shellIntegration.ts |
Node-side PTY process wrapper, binary resolution, shell-integration env |
src/terminal/oscHandler.ts |
Shared workaround for xterm.js's non-functional public registerOscHandler |
src/terminal/CommandTracker.ts |
OSC 133 command-boundary/exit-code tracking |
src/terminal/CwdTracker.ts |
OSC 7 working-directory tracking |
src/terminal/TerminalLinks.ts |
Clickable file paths and URLs in terminal output |
src/terminal/fontCatalog.ts |
Installed-monospace-font detection for the settings dropdown |
src/terminal/WikiLinkAutocomplete.ts |
[[ fuzzy note picker inside the terminal |
src/terminal/TerminalColorPicker.ts, colorPalette.ts, tabHeaderColor.ts |
Color-swatch popover, preset palette, and the native-tab tint/title-refresh/needs-attention dot |
src/server/ReviewServer.ts, diff.ts |
Local hook-bridge HTTP server, diff computation |
src/state/PendingChangesStore.ts, ActionLog.ts |
In-memory pending/undo state, persisted action log |
src/state/ClosedTerminalBuffer.ts |
Ring buffer backing "Rescue closed terminal" |
src/hooks/provisionSettings.ts, provisionResources.ts, types.ts |
.claude/settings.local.json provisioning, writing resources/ out on load, hook payload types |
src/editor/inlineDiff.ts, openWithDiff.ts |
CodeMirror 6 inline diff overlay, and opening a changed file |
src/util/systemOpen.ts |
Handing a file to the OS, for paths Obsidian can't open in a leaf |
src/onboarding/gettingStarted.ts |
Bundled first-run and getting-started copy |
src/backlinks/breakage.ts |
Backlink-breakage detection |
src/git/gitDiff.ts |
Git HEAD baseline diff |
src/git/gitStatus.ts |
Per-turn git status snapshots behind shell-change detection |
src/state/SessionLog.ts, sessionLogFormat.ts |
Per-session notes in the vault: the vault I/O, and the pure formatters that fix their exact wording |
src/claude/headlessAssist.ts |
Standalone claude -p calls for explain/suggest-fix |
src/modals/ |
Action log, command-help, confirm, rename, rescue-terminal, and getting-started modals |
test/, scripts/buildTests.mjs |
Node --test suite and the esbuild step that makes it runnable (see Development) |
resources/pty_helper.py |
PTY allocation helper (spawned by the plugin) |
resources/hook-bridge.sh |
PreToolUse hook → local server bridge |
resources/turn-end-bridge.sh |
Stop hook → local server bridge (end of turn) |
resources/notification-bridge.sh |
Notification hook → local server bridge (needs-attention dot) |
resources/shell-integration/{zsh,bash}/ |
Shell rc scripts for command tracking and cwd tracking |
MIT. See LICENSE.