Skip to content

Bundle hooks, MCP bridge, and skills as a plugin for Claude and Codex - #120

Draft
big-guy wants to merge 4 commits into
ness-dev:mainfrom
big-guy:hooks-install-rework
Draft

Bundle hooks, MCP bridge, and skills as a plugin for Claude and Codex#120
big-guy wants to merge 4 commits into
ness-dev:mainfrom
big-guy:hooks-install-rework

Conversation

@big-guy

@big-guy big-guy commented May 28, 2026

Copy link
Copy Markdown
Collaborator

Fixes #44

This is an unblocking refactor to more easily support multiple accounts with Claude.

For Claude, we do not need to modify anything.
For Codex, we still need to modify global files, but we can do this with codex commands vs editing the files manually.

On uninstall, we still edit files manually for codex.

NOTE: Testing this can be annoying because when you run Harness, it deletes things from the global configuration. When you switch back to an older branch, the hooks are not re-installed.

I haven't gone through the onboarding/fresh install step.

Summary

Three commits that move Harness's status-detection infrastructure from per-agent ad-hoc installs (writing into ~/.claude/settings.json and ~/.codex/hooks.json) into a single bundled plugin tree at resources/plugins/harness-status/ consumed by both agents.

  • Claude loads the plugin via --plugin-dir <bundled-path> on every spawn (xterm + json-mode). No user-file install, no consent prompt, no merge against user-authored hooks.
  • Codex consumes the same plugin via codex plugin marketplace add + codex plugin add at consent time. Gated by an in-tab install hint that only appears for actual Codex sessions (no top-of-app banner).
  • Skills previously embedded in the Harness system prompt now live in the plugin's skills/ dir so both agents auto-discover them.

What changed for Codex specifically

  • `hooks:accept` runs the two `codex plugin` CLI commands, then probes (`plugin list`, cache dir, `config.toml` trust entries) and surfaces the result in Settings.
  • Codex tabs get `-c mcp_servers.harness-control.command="" -c mcp_servers.harness-control.args=[""] -c mcp_servers.harness-control.env={…literals…}` appended at spawn — Codex's `.mcp.json` parser does no interpolation, so per-spawn literals are the only way to reach the bridge.
  • Hook trust stays a manual step; Settings card instructs the user to accept Codex's TUI prompt and click Re-check.
  • `CODEX_HOME` set from `$HOME` at boot when not already set — fixes isolated dev sessions where macOS's `getpwuid_r` would otherwise dodge the env override.

Migration

Two one-shot strips run on first boot post-upgrade, gated by persisted flags so they don't re-run:

  • `hooksMigratedToPlugin`: strips Harness entries from `~/.claude/settings.json` + per-worktree `.claude/settings.local.json`.
  • `codexPluginMigrated`: strips Harness entries from `~/.codex/hooks.json` + per-worktree `.codex/hooks.json`. Positive strip → infer consent = 'accepted' so prior consenters aren't re-prompted.

Test plan

  • `npm run typecheck` passes
  • `npx vitest run` — 119 files, 1341 tests pass
  • `npx electron-vite build` clean
  • End-to-end verified in dev with isolated `$HOME` (`HOME=/tmp/harness-iso-… npm run dev`):
    • Plugin install writes to isolated `.codex/config.toml`, not the real user's
    • Hooks fire under Codex (`/tmp/harness-status/.ndjson` populated)
    • MCP bridge reachable from Codex sessions (`mcp__harness-control__*` tools available)
    • Uninstall strips `[hooks.state."harness-status@harness:…"]` entries cleanly
    • Re-check correctly reports trust state after the TUI prompt
  • Smoke test on a clean machine without legacy `~/.codex/hooks.json` trust hashes to confirm the in-tab install + TUI trust flow end to end

@big-guy
big-guy force-pushed the hooks-install-rework branch from c97d799 to 5f5bfb5 Compare May 28, 2026 05:55
@big-guy big-guy self-assigned this May 28, 2026
@big-guy
big-guy force-pushed the hooks-install-rework branch 4 times, most recently from 264649b to 2cb74ef Compare June 4, 2026 05:07
big-guy and others added 4 commits June 4, 2026 17:47
…in skills

The DEFAULT_HARNESS_SYSTEM_PROMPT used to enumerate every harness-control
MCP tool and inline its workflow guidance (clickables-first click pattern,
create_shell vs Bash heuristic, how to write a good initialPrompt). That
content is always-on context every Claude turn pays for, even when the
task has nothing to do with browser tabs or shell management.

Move the workflow content into three skills shipped in the bundled
plugin under resources/plugins/harness-status/skills/:

  - harness-browser  — clickables → click_tab pattern, screenshot as
                       verification-only, scroll/type workflow
  - harness-shell    — create_shell vs Bash, list before spawning,
                       read_shell_output filtering
  - harness-worktree — when to spin off a worktree, how to write an
                       initialPrompt the new session can actually act on

Claude Code auto-discovers each skill via the same --plugin-dir flag
that already loads our hooks and harness-control MCP server, so no
additional wiring is needed. Skills are pulled in on-demand by
description match, so unrelated turns pay zero token cost.

Shrinks DEFAULT_HARNESS_SYSTEM_PROMPT from ~3.8KB to a 418-char
identity line that points at the skills, and trims
DEFAULT_HARNESS_SYSTEM_PROMPT_MAIN to drop the bits that duplicated
harness-shell content. Tool-level descriptions on the MCP side still
cover the "what does this do" surface — the skills layer on the
"when and why."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move Claude status hooks and the harness-control MCP bridge into a
bundled Claude Code plugin at resources/plugins/harness-status/. Every
Claude spawn (xterm + json-mode) now passes --plugin-dir, so there's no
user-file install to consent to and no merge against user-authored
hooks. The plugin tree's .mcp.json + servers/mcp-bridge.js replace the
previous per-terminal mcp.json materialization under userData.

The plugin tree also doubles as a Codex plugin marketplace
(.agents/plugins/marketplace.json wraps the same harness-status dir).
Codex 0.133+ reads the .claude-plugin/plugin.json manifest, hooks.json,
.mcp.json and skills/ from the same tree without modification.
codex-plugin.ts scaffolds the boot-time install path (marketplace add
+ plugin add) but is not yet wired into the Codex consent flow — that
landing is a follow-up.

Other changes:
- One-shot boot migration strips legacy Harness entries from
  ~/.claude/settings.json + per-worktree .claude/settings.local.json
  (gated by config.hooksMigratedToPlugin).
- Hooks consent slice is now Codex-only; Claude needs no consent.
- New harness-control-env.ts builds the MCP server env (port, token,
  per-terminal scope) so both spawn paths set it consistently.
- harnessPluginMarketplaceRoot() resolves the marketplace root for the
  forthcoming Codex install path.
…overrides

Codex consumes the same plugin tree as Claude (resources/plugins/
harness-status/), but the integration path is different in three
fundamental ways the implementation had to discover empirically:

1. Codex does NOT interpolate `${VAR}` in .mcp.json — not user env
   (${HARNESS_NODE_EXEC}), not its own injected vars (${CLAUDE_PLUGIN_ROOT}).
   Templates are passed verbatim to execve, and the plugin's source
   .mcp.json (which Claude expands correctly via --plugin-dir) would
   fail with "MCP startup failed: No such file or directory" if Codex
   tried to spawn it. We neutralize the cached .mcp.json post-install
   to `{"mcpServers": {}}` and register harness-control per-Codex-spawn
   via `-c mcp_servers.harness-control.command="..."` literal overrides.

2. Codex spawns MCP subprocesses with a stripped env — only
   HOME/PATH/PWD/USER/LANG inherit. The HARNESS_* vars set on the
   agent PTY don't reach the bridge. The per-spawn `-c` override
   bakes literal values directly into the MCP env block.

3. Codex's `~` resolution uses getpwuid_r (system passwd database)
   rather than $HOME on macOS, so an isolated $HOME doesn't reach the
   Codex binary. We set process.env.CODEX_HOME = $HOME/.codex at boot
   when not already set — idempotent for the common case, fixes
   isolation for dev sessions, propagates to all child processes.

Install path:
- `hooks:accept` IPC runs `codex plugin marketplace add <bundled-root>`
  + `codex plugin add harness-status@harness`. Both write to
  $CODEX_HOME/config.toml; cleanly reversible via the matching remove
  commands.
- Boot logic re-runs the install every launch when consent === 'accepted'.
  Codex's plugin cache never auto-refreshes, so re-running is how we
  propagate new Harness releases to the cache.
- Spawn args: agents/codex.ts buildSpawnArgs appends -c overrides for
  command, args, and env when opts.harnessControl is set.

Verification (Settings → Codex → "Harness status plugin"):
- Plugin enabled in Codex (`codex plugin list --marketplace harness`)
- Hooks file present in cache
- Hooks trusted by Codex (key-based presence check —
  `[hooks.state."harness-status@harness:hooks/hooks.json:<event>:N:M"]`
  with non-empty trusted_hash for every Codex-recognized event from
  hooks.json; Notification is filtered out as Claude-only).

Trust:
- We do NOT auto-trust. Codex's trust prompt is TUI-only; there's no
  CLI command or flag to grant trust non-interactively. The Settings
  card surfaces inline guidance when hooks aren't yet trusted: open a
  Codex tab, accept the "Hooks need review / Trust all and continue"
  prompt, then Re-check.

Uninstall:
- `codex plugin remove` + `codex plugin marketplace remove`
- Strip [hooks.state] entries from $CODEX_HOME/config.toml by
  `harness-status@harness:` key prefix (Codex's `trusted_hash` is over
  a normalized form we can't reproduce, so key-based matching is the
  reliable signal). A reinstall re-prompts for trust review rather
  than silently inheriting prior consent via content-hash match.

UI:
- Install prompt lives as an in-tab overlay in XTerminal.tsx
  (`agentKind === 'codex' && hooksConsent === 'pending'`), mirroring
  the Claude "Switch to Chat mode" hint. No top-of-app banner —
  Claude-only users never see it.
- Settings card shows the actual CLI commands Harness runs, the
  verification panel, and a Re-check button.
- Card title: "Harness status plugin" (it's a plugin Harness ships
  for Codex to call back into Harness, not a Codex-authored plugin).

Codex agent module (agents/codex.ts):
- Removed installHooks/uninstallHooks/hooksInstalled (~150 lines of
  dead code from the pre-plugin ~/.codex/hooks.json install path).
  stripHooksFromWorktree reduced to a one-line delegate satisfying
  the AgentModule contract; effectively dead post-migration.
- Sessions dir lookup honors CODEX_HOME.

Migration:
- `codexPluginMigrated` flag gates a one-shot strip of legacy
  ~/.codex/hooks.json + per-worktree .codex/hooks.json Harness entries
  on first boot post-upgrade. Inferred consent (positive legacy strip
  → 'accepted') so prior consenters don't get re-prompted.

Onboarding:
- Move the Codex plugin install prompt from a top-level onboarding step
  into an inline sub-section under the agent picker, mirroring the
  Claude interface (chat/terminal) toggle. Onboarding collapses from 4
  steps to 3 so Claude-only users don't see a Codex question they
  don't need to answer.
Add SessionStart to the Claude status plugin's hook set, mirroring Codex.
A freshly started/resumed session derives to 'waiting', and its payload
carries session_id so the generic discovery in tailLog picks up the
session ID before the first prompt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@big-guy
big-guy force-pushed the hooks-install-rework branch from 2cb74ef to 63259e6 Compare June 4, 2026 21:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Investigate replacing global hooks with Harness plugin

1 participant