The agent side of the lich integration. lich is a harness that orchestrates agent CLI sessions; this companion plugin gives it eyes and hands inside each session. It installs on Claude Code, OpenAI Codex, Antigravity CLI, opencode, omp (oh-my-pi) and Crush from this same repository. Every hook implements a contract that is canonical in the lich repository (docs/hooks/ there); the docs in docs/ here point at each contract and describe the client side:
- session state —
busy/doneon the session card (UserPromptSubmit/PreInvocation/Stop) - session start — persists the agent's session id on the lich session (
SessionStart/PreInvocation) - session title — names the card after the session's own title (
Stop) - session touched — refreshes the card's git status right after file-mutating tools (
PostToolUse)
The four are what Claude Code, Codex, Antigravity, opencode and omp report. Crush reports two of them — its session id and the git-status refresh — because PreToolUse is the only event it has, and a state nothing can end is worse on a card than no state. omp reports all four minus the bell: no event of its own for "your turn" has been measured yet, so its card shows a spinner while the agent waits on you. docs/providers.md has the event mapping per harness.
On opencode it also carries the other direction: the seven operations lich offers a session for driving the sessions beside it — list, send, wait, reply, open, close, and read the worktrees — as tools of opencode's own. Claude Code and Codex get those as MCP tools lich registers when it spawns them; opencode cannot be told about a server on its command line, so they are defined in the module instead. docs/opencode-tools.md has the list and the two cases where they are deliberately absent.
It also ships skills for the parts of lich you configure from inside a session:
- theme (
/lich:themein Claude Code; on Codex and Antigravity thethemeskill loads from its description) — write, port or fix a lich color theme: the app tokens, the xterm palette, where the file goes, and a validator for the rules that otherwise fail silently
.claude-plugin/plugin.json # plugin manifest, Claude Code
.claude-plugin/marketplace.json # marketplace, Claude Code
.codex-plugin/plugin.json # plugin manifest, Codex
.agents/plugins/marketplace.json # marketplace, Codex
plugin.json # plugin manifest, Antigravity (name fixed, must sit at the root)
hooks.json # hook registration, Antigravity (likewise)
hooks/hooks.json # hook registration, Claude Code
hooks/codex-hooks.json # hook registration, Codex
hooks/crush-hooks.json # hook registration, Crush (merged into crush.json by hand)
hooks/report-state.sh # session-state hook
hooks/report-tool.sh # session-state hook: the tool a turn is running
hooks/report-session-start.sh # session-start hook
hooks/report-title.sh # session-title hook
hooks/report-touched.sh # session-touched hook
opencode/lich.js # opencode client: all four reports plus the seven tools, one module
omp/lich.js # omp client: the reports, one module
docs/ # client-side docs, one per contract
skills/theme/ # theme skill: SKILL.md, template.json, validate.mjs
tests/ # hook payloads asserted against lich's fixtures
One set of hook scripts serves Claude Code, Codex, Antigravity and Crush — they live in hooks/. Claude Code and Codex reach them through $CLAUDE_PLUGIN_ROOT/hooks/<script>, a variable Codex sets too; Crush ships a placeholder the user replaces. Antigravity sets no such variable — it runs a hook with the working directory set to the plugin root, so its registration spells hooks/<script> relative to that, and it is the reason plugin.json and hooks.json sit at the root: Antigravity fixes both names and looks for them nowhere else. opencode and omp run no commands: in both, what gets loaded is a JavaScript module, so each has a single-file client — opencode/lich.js and omp/lich.js — sending the same payloads to the same endpoints. docs/providers.md maps the layout and every harness's event names.
Add this repository as a marketplace and install the plugin:
claude plugin marketplace add omartelo/lich-plugin
claude plugin install lich@lich-pluginThe same works inside a session with /plugin marketplace add omartelo/lich-plugin followed by /plugin install lich@lich-plugin.
codex plugin marketplace add omartelo/lich-plugin
codex plugin add lich@lich-pluginThen start a new session and run /hooks to review and trust the plugin's hooks — Codex does not run a plugin's hooks until you do, so until then the plugin is installed but silent. Hooks themselves are stable and on by default in current Codex; on older versions set [features] hooks = true in ~/.codex/config.toml.
A plugin is a directory holding a plugin.json, under a plugins/ folder in a customization root. Symlink the clone into one — globally, or into a project's .agents/ to share it with the team:
mkdir -p ~/.gemini/config/plugins
ln -s "$PWD"~/.gemini/config/plugins/lichThat is the whole install: the hooks come from hooks.json beside the manifest, and the theme skill from skills/. agy plugin validate . checks the bundle from the clone before you link it, agy plugin list shows it afterwards, and agy plugin disable lich turns it off again.
Without the plugin, hooks.json can be merged into ~/.gemini/config/hooks.json (global) or .agents/hooks.json (per project) — but a hook's working directory is the folder holding the hooks.json that declared it, so replace each hooks/ prefix with the absolute path to this clone.
opencode has no marketplace: a plugin is a file in its plugin directory, so dropping it there is the install.
mkdir -p ~/.config/opencode/plugin
curl -fsSL -o ~/.config/opencode/plugin/lich.js \
https://raw.githubusercontent.com/omartelo/lich-plugin/main/opencode/lich.jsPer project instead of globally, use .opencode/plugin/lich.js. Updating means fetching the file again.
omp has no marketplace of its own either, and its --hook and --extension flags are one list of modules. It scans ~/.omp/agent/extensions/ without being told, so dropping the file there is the install:
mkdir -p ~/.omp/agent/extensions
curl -fsSL -o ~/.omp/agent/extensions/lich.js \
https://raw.githubusercontent.com/omartelo/lich-plugin/main/omp/lich.jsTo keep it somewhere else, name the path instead — omp config set extensions '["/path/to/lich.js"]', which writes extensions: in ~/.omp/agent/config.yml — or pass --hook /path/to/lich.js for a single run. Per project, .omp/extensions/lich.js works the same way. Updating means fetching the file again.
Crush has no plugin system either: its hooks live in your own crush.json. Clone this repository, then merge hooks/crush-hooks.json into ~/.config/crush/crush.json (global) or the project's crush.json, replacing <lich-plugin> with the absolute path of the clone:
command is resolved against the working directory, not the config file, so a global install needs the absolute path. Crush reports no session state and no title — see docs/providers.md for why.
git clone https://github.com/omartelo/lich-plugin.gitThen either load it for a single Claude Code session:
claude --plugin-dir ./lich-pluginor register the clone as a local marketplace for a persistent install:
claude plugin marketplace add ./lich-plugin
claude plugin install lich@lich-plugin
codex plugin marketplace add ./lich-plugin
codex plugin add lich@lich-pluginOutside lich (env vars absent) the hooks are a no-op — the plugin is safe to install globally.
claude --plugin-dir .node --test tests/*.test.mjsEvery hook script runs as a real subprocess, from the command line its registration spells, against a stub HTTP server — and the body it POSTs is asserted against lich's contract fixtures: an accepted shape, never a rejected one, the right endpoint and token, plus the client rules (no lich environment → no report; exit 0 when lich answers 500 or refuses the connection). The opencode and omp modules are imported instead of spawned and fed the events a real run of each emits, against the same fixtures. All three share tests/contract.mjs. The fixtures are vendored in tests/fixtures/ by tests/refresh-fixtures.sh; CI diffs them against upstream so a contract that moves in lich goes red here.
{ "hooks": { "PreToolUse": [ { "name": "lich session id", "command": "/home/you/src/lich-plugin/hooks/report-session-start.sh crush", "timeout": 5 }, { "name": "lich git-status refresh", "matcher": "^(edit|write|multiedit|bash)$", "command": "/home/you/src/lich-plugin/hooks/report-touched.sh", "timeout": 5 } ] } }