Two steps that are genuinely separate, and conflating them is the mistake worth avoiding: installing the plugin puts code and wiring on a machine, and marking a root decides where state lives and which sessions can see each other. Installing on its own does nothing observable, on purpose.
From GitHub:
/plugin marketplace add teerakarna/session-exchange
/plugin install session-exchange@session-exchange
From a clone, which is the better option while the repo is moving. Wherever you keep clones is where this goes; nothing in the plugin cares:
/plugin marketplace add /path/to/your/session-exchange
/plugin install session-exchange@session-exchange
The marketplace and the plugin share a name; session-exchange@session-exchange is not a typo.
Installing produces files in two places, and only one of them is the plugin:
| Path | What it is |
|---|---|
~/.claude/plugins/marketplaces/session-exchange/ |
the marketplace clone: the whole repo, including tests, docs and CI. One per marketplace, not per plugin |
~/.claude/plugins/cache/session-exchange/session-exchange/<version>/ |
the installed plugin: a copy of the repo's plugin/ directory, and what CLAUDE_PLUGIN_ROOT resolves to |
~/.claude/plugins/installed_plugins.json records the second as installPath, along with the commit it
came from, which is the authoritative answer on any machine.
Note the <version> in the second path. It comes from plugin/.claude-plugin/plugin.json, so it
changes on every version bump. Do not hardcode it anywhere.
The hooks need neither path. They find their own code through CLAUDE_PLUGIN_ROOT, which is why nothing
in this plugin has a path configured.
Nothing is written to ~/.claude/settings.json. The hooks are declared in the plugin's own
hooks/hooks.json and travel with it, which is the reason this is a plugin rather than four scripts and
a settings edit.
Restart the session, or start a new one. SessionStart has already fired for the session you
installed from.
A root is a directory containing .claude/exchange.json. From anywhere inside the tree you want
coordinated:
/exchange init
It prints the directory it would mark, anything else it considered, and anything it is refusing, before writing. What it is choosing between:
- It defaults to the nearest ancestor holding a
CLAUDE.md, strictly above the enclosing git repo. A repo-scoped exchange coordinates nothing, because the sessions that need to see each other are in sibling repos. - It refuses a directory whose sibling children are themselves workspaces rather than repos. Marking
that one directory would give each workspace sight of the others' sessions and handoffs, which is the
single thing root resolution exists to prevent.
--forcegets past it and says so.
Pass a directory explicitly if you disagree with the default: init /path/to/tree.
For a one-off or a single pane, CC_EXCHANGE_ROOT overrides everything and needs no marker at all.
Useful for trying it somewhere without leaving a file behind.
/exchange doctor
What to read in the output:
rootnames the directory and the rule that resolved it. If that saysby markerand points somewhere you did not expect, resolution walked further up than you thought.stepslists all seven migration steps derived from live state.[?]means the check is not answerable from here and prints why, rather than being omitted: a diagnostic that quietly skips a check reads exactly like one that passed it.[-]means there is nothing to do: step 4 shows it on a root whose marker names nolegacy_ledger. If that root did keep a markdown ledger, add the key, or the import reads as not needed when it was only never declared.DOUBLE FIREmeans a legacy hook is still wired alongside the plugin. Presence and handoffs will render twice, and a stalled migration becomes indistinguishable from a finished one. This also appears in every session's injected context until it is gone.migrate --step 7removes it.
Then start a new session in the tree and confirm a claim appeared:
ls <root>/.claude/exchange/sessions/Silence with no root marked is correct and not a fault. If doctor says there is no environment root
and you meant there to be one, you are below a marker that does not exist, or above the one you made.
The plugin is installed per machine. The root marker and everything under
<root>/.claude/exchange/ lives in the tree, so if that tree is a repo, decide deliberately whether
the state is committed or ignored. Claims are ephemeral and machine-specific; handoffs are the part
with durable value.
Sessions never read across roots in either direction, so two trees on one machine stay separate with no configuration, and the same plugin serves both.
In a session, use /exchange: it resolves its own location through CLAUDE_PLUGIN_ROOT and needs no
alias, no PATH entry and no configuration. Everything below is only for a shell prompt.
The plugin deliberately puts nothing on your PATH, because a plugin that edits a shell profile has overstepped. Adding an alias yourself is fine, but not pointed at the installed copy: that path carries the plugin version and breaks silently at the next bump, which is the worst kind of break for something you type from memory.
Point it at a clone instead. The clone is what you edit and test against anyway, and it has no version in its path:
alias exchange='python3 /path/to/your/session-exchange/plugin/lib/cli.py'If you would rather run the copy that is actually installed, derive the path rather than typing it, so it survives a version bump:
exchange() {
local root
root=$(python3 -c '
import json, pathlib
data = json.loads((pathlib.Path.home() / ".claude/plugins/installed_plugins.json").read_text())
entries = data["plugins"]["session-exchange@session-exchange"]
print(max(entries, key=lambda e: e["installedAt"])["installPath"])
') || return 1
python3 "$root/lib/cli.py" "$@"
}Both run the same code while the clone is in sync with what is installed. When they are not, the second is the honest one, which is the whole reason to prefer it while the plugin is still moving.
/plugin uninstall session-exchange
/plugin marketplace remove session-exchange
That removes the code and the wiring. State is left alone, because deleting somebody's handoffs as a side effect of uninstalling a tool would be the wrong default. To remove it too:
rm -rf <root>/.claude/exchange <root>/.claude/exchange.jsonCheck what is in <root>/.claude/exchange/ first. EXCHANGE.md is hand-maintained narrative and is the
one file in there nothing can regenerate.