Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 37 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ src/
│ ├── worktrees.ts # list, repoRoots, pending FSM entries
│ ├── terminals.ts # statuses, pendingTools, shellActivity, panes, lastActive
│ ├── onboarding.ts # quest step
│ ├── hooks.ts # consent + justInstalled
│ ├── hooks.ts # Codex hooks consent (Claude rides on --plugin-dir)
│ ├── updater.ts # status (checking/available/downloading/…)
│ ├── repo-configs.ts # byRepo: per-repo .harness.json contents
│ └── *.test.ts # vitest reducer tests, one per slice
Expand Down Expand Up @@ -195,10 +195,6 @@ Some main-side modules subscribe to the store and react to events:
`terminals/*` and `prs/*` events, computes per-worktree effective
state, debounces `lastActive` updates, dedups `recordActivity` calls
to `activity.ts`.
- **`installHooksForAcceptedWorktrees`** — small subscriber in
`main/index.ts` that listens for `worktrees/listChanged` and
`hooks/consentChanged`, installs hooks into any new worktree if
consent is `'accepted'`.

Construction order in `main/index.ts` matters: `PanesFSM` is constructed
**before** `WorktreesFSM` because the latter's `onWorktreeCreated`
Expand Down Expand Up @@ -288,11 +284,42 @@ event type if you're trying to find where something happens.
## How status detection works

The reliable status (processing / waiting / needs-approval) comes from
**Claude Code hooks** that we install into each worktree's
`.claude/settings.local.json`. The hooks write a status JSON to
`/tmp/harness-status/<terminal-id>.json` and the main process watches that
directory via `fs.watch`. The hook script uses `$CLAUDE_HARNESS_ID` env var
which the PtyManager sets when spawning each terminal.
**Claude Code hooks** that emit one NDJSON line per event to
`/tmp/harness-status/<terminal-id>.ndjson`. The main process watches that
directory via `fs.watch` and tails the file (see `src/main/hooks.ts`).
The hook command env-gates on `$HARNESS_TERMINAL_ID` (set by PtyManager
when it spawns each tab), so sessions outside Harness no-op cleanly.

**How the hooks get loaded:**

- **Claude** — Harness ships a local Claude Code plugin under
`resources/plugins/harness-status/` (manifest at
`.claude-plugin/plugin.json`, hooks at `hooks/hooks.json`). Every
Claude spawn — both the xterm path and json-mode — passes
`--plugin-dir <bundled-path>`. Nothing is written to
`~/.claude/settings.json`; no consent is required. The plugin is
resolved via `harnessPluginDir()` in `src/main/claude-plugin.ts`
(mirrors the `process.resourcesPath` pattern used by `mcp-bridge.js`).
A drift-detection test in `src/main/claude-plugin.test.ts` asserts the
shipped `hooks.json` matches what `makeHookCommand()` would generate
today so the two can't silently diverge.
- **Codex** — Codex 0.133+ reads the **same** plugin tree as Claude.
No `--plugin-dir` flag exists for Codex, so we register the bundled
directory as a local marketplace via `codex plugin marketplace add
<bundled-root>` and enable the plugin with `codex plugin add
harness-status@harness`. Both commands write entries to
`~/.codex/config.toml`, which is why this path is still gated by the
consent banner / Settings card. The `hooks/consent` slice +
`hooks:accept|decline|uninstall` IPC handlers wire the user choice;
see `src/main/codex-plugin.ts` for the install / uninstall helpers.
Two one-shot boot migrations sweep dead state from prior installs:
`config.hooksMigratedToPlugin` strips legacy Claude entries from
`~/.claude/settings.json` + per-worktree `.claude/settings.local.json`;
`config.codexPluginMigrated` strips legacy Harness entries from
`~/.codex/hooks.json` + per-worktree `.codex/hooks.json`.
After acceptance, `installCodexPlugin()` runs on every boot —
idempotent, and the only way to force Codex's plugin cache to pick up
a new Harness release (the cache never refreshes automatically).

## How performance debugging works

Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,13 @@
"**/node_modules/@anthropic-ai/claude-code-*/**/*"
],
"extraResources": [
{
"from": "resources/mcp-bridge.js",
"to": "mcp-bridge.js"
},
{
"from": "resources/permission-prompt-mcp.js",
"to": "permission-prompt-mcp.js"
},
{
"from": "resources/plugins",
"to": "plugins"
}
],
"mac": {
Expand Down
2 changes: 1 addition & 1 deletion resources/mcp-bridge.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { dirname, join } from 'path'

const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const BRIDGE = join(__dirname, 'mcp-bridge.js')
const BRIDGE = join(__dirname, 'plugins', 'harness-status', 'servers', 'mcp-bridge.js')

function startStub(handler) {
return new Promise((resolve) => {
Expand Down
10 changes: 10 additions & 0 deletions resources/plugins/.agents/plugins/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "harness",
"version": "1.0.0",
"plugins": [
{
"name": "harness-status",
"source": "./harness-status"
}
]
}
5 changes: 5 additions & 0 deletions resources/plugins/harness-status/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "harness-status",
"version": "1.0.0",
"description": "Integrate with Harness for up-to-date status"
}
18 changes: 18 additions & 0 deletions resources/plugins/harness-status/.mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"mcpServers": {
"harness-control": {
"command": "${HARNESS_NODE_EXEC}",
"args": ["${CLAUDE_PLUGIN_ROOT}/servers/mcp-bridge.js"],
"env": {
"ELECTRON_RUN_AS_NODE": "1",
"HARNESS_PORT": "${HARNESS_PORT}",
"HARNESS_TOKEN": "${HARNESS_TOKEN:-}",
"HARNESS_TERMINAL_ID": "${HARNESS_MCP_TERMINAL_ID}",
"HARNESS_SESSION_ID": "${HARNESS_MCP_TERMINAL_ID}",
"HARNESS_WORKTREE_ID": "${HARNESS_WORKTREE_ID:-}",
"HARNESS_REPO_ROOT": "${HARNESS_REPO_ROOT:-}",
"HARNESS_IS_MAIN": "${HARNESS_IS_MAIN:-}"
}
}
}
}
70 changes: 70 additions & 0 deletions resources/plugins/harness-status/hooks/hooks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "bash -c 'h=\"$HARNESS_TERMINAL_ID\"; [ -z \"$h\" ] && h=\"$CLAUDE_HARNESS_ID\"; [ -z \"$h\" ] && exit 0; d=/tmp/harness-status; mkdir -p \"$d\"; p=$(cat); [ -z \"$p\" ] && p=null; printf \"{\\\"event\\\":\\\"SessionStart\\\",\\\"ts\\\":%s,\\\"payload\\\":%s}\\n\" \"$(date +%s)\" \"$p\" >> \"$d/$h.ndjson\"'",
"timeout": 5
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "bash -c 'h=\"$HARNESS_TERMINAL_ID\"; [ -z \"$h\" ] && h=\"$CLAUDE_HARNESS_ID\"; [ -z \"$h\" ] && exit 0; d=/tmp/harness-status; mkdir -p \"$d\"; p=$(cat); [ -z \"$p\" ] && p=null; printf \"{\\\"event\\\":\\\"UserPromptSubmit\\\",\\\"ts\\\":%s,\\\"payload\\\":%s}\\n\" \"$(date +%s)\" \"$p\" >> \"$d/$h.ndjson\"'",
"timeout": 5
}
]
}
],
"PreToolUse": [
{
"hooks": [
{
"type": "command",
"command": "bash -c 'h=\"$HARNESS_TERMINAL_ID\"; [ -z \"$h\" ] && h=\"$CLAUDE_HARNESS_ID\"; [ -z \"$h\" ] && exit 0; d=/tmp/harness-status; mkdir -p \"$d\"; p=$(cat); [ -z \"$p\" ] && p=null; printf \"{\\\"event\\\":\\\"PreToolUse\\\",\\\"ts\\\":%s,\\\"payload\\\":%s}\\n\" \"$(date +%s)\" \"$p\" >> \"$d/$h.ndjson\"'",
"timeout": 5
}
]
}
],
"PostToolUse": [
{
"hooks": [
{
"type": "command",
"command": "bash -c 'h=\"$HARNESS_TERMINAL_ID\"; [ -z \"$h\" ] && h=\"$CLAUDE_HARNESS_ID\"; [ -z \"$h\" ] && exit 0; d=/tmp/harness-status; mkdir -p \"$d\"; p=$(cat); [ -z \"$p\" ] && p=null; printf \"{\\\"event\\\":\\\"PostToolUse\\\",\\\"ts\\\":%s,\\\"payload\\\":%s}\\n\" \"$(date +%s)\" \"$p\" >> \"$d/$h.ndjson\"'",
"timeout": 5
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "bash -c 'h=\"$HARNESS_TERMINAL_ID\"; [ -z \"$h\" ] && h=\"$CLAUDE_HARNESS_ID\"; [ -z \"$h\" ] && exit 0; d=/tmp/harness-status; mkdir -p \"$d\"; p=$(cat); [ -z \"$p\" ] && p=null; printf \"{\\\"event\\\":\\\"Stop\\\",\\\"ts\\\":%s,\\\"payload\\\":%s}\\n\" \"$(date +%s)\" \"$p\" >> \"$d/$h.ndjson\"'",
"timeout": 5
}
]
}
],
"Notification": [
{
"hooks": [
{
"type": "command",
"command": "bash -c 'h=\"$HARNESS_TERMINAL_ID\"; [ -z \"$h\" ] && h=\"$CLAUDE_HARNESS_ID\"; [ -z \"$h\" ] && exit 0; d=/tmp/harness-status; mkdir -p \"$d\"; p=$(cat); [ -z \"$p\" ] && p=null; printf \"{\\\"event\\\":\\\"Notification\\\",\\\"ts\\\":%s,\\\"payload\\\":%s}\\n\" \"$(date +%s)\" \"$p\" >> \"$d/$h.ndjson\"'",
"timeout": 5
}
]
}
]
}
}
File renamed without changes.
36 changes: 36 additions & 0 deletions resources/plugins/harness-status/skills/harness-browser/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
name: harness-browser
description: Drive Harness's embedded browser tabs to verify UI changes, debug a rendered page, click through a flow, or inspect a dev server. Use when the user mentions a browser, a URL, the rendered UI, clicking/typing in a page, taking a screenshot, or verifying a web change works.
---

# Browser tabs in Harness

Harness embeds browser tabs alongside your terminal. They're scoped to the current worktree and driven by the `harness-control` MCP tools. Prefer these over blind `curl`/`fetch` or shelling out to `open <url>` — `curl` can't render JS or inspect DOM state, and `open` launches the user's default browser outside Harness where you can't see what happened.

## Click targeting workflow

**Prefer `get_tab_clickables` → match by role + name → call `click_tab(cx, cy)`** for anything you want to click. It's far cheaper than a screenshot + vision pass and far more reliable for real DOM targets.

The clickables snapshot is:
- In-viewport only — if the target isn't there, `scroll_tab` first then re-snapshot
- Capped at 500 items
- Includes elements inside open shadow roots
- Returns `{role, name, cx, cy, w, h}` with the click center already computed

Reserve `screenshot_tab` for:
- Confirming a click had the visual effect you wanted
- Targets without accessible names (canvas/SVG/images) where clickables can't help

Default screenshot format is JPEG quality 70 (context-efficient). Ask for `format: 'png'` only when lossless matters. Screenshot dimensions match the CSS viewport, so any coords you read off a screenshot can be passed straight to `click_tab`.

## Typing into fields

`click_tab` on the field first to focus it, then `type_tab`. `type_tab` also accepts a `key` arg (`Enter`, `Tab`, `Backspace`, `ArrowDown`, …) for submitting forms or navigating menus.

## Tools at a glance

- `create_browser_tab` — open a new tab in this worktree (optionally navigating to a URL)
- `list_browser_tabs`, `get_tab_url`, `get_tab_dom`, `get_tab_console_logs` — inspect
- `navigate_tab`, `back_tab`, `forward_tab`, `reload_tab` — drive
- `get_tab_clickables`, `click_tab`, `type_tab`, `scroll_tab`, `show_cursor` — interact
- `screenshot_tab` — visual verification
28 changes: 28 additions & 0 deletions resources/plugins/harness-status/skills/harness-shell/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
name: harness-shell
description: Use Harness shell tabs (create_shell, read_shell_output, kill_shell) instead of Bash for long-running processes — dev servers, watchers, `tail -f`, REPL-style tools, long builds. Use when the user asks to start a dev server, tail logs, run a watcher, or anything that wouldn't naturally exit within a few seconds.
---

# Shell tabs in Harness

For processes that wouldn't naturally exit within a few seconds — dev servers, watchers, `tail -f`, REPL-style tools, long builds — use the harness-control shell tools instead of Bash.

## Why not Bash?

- Bash either **blocks** until the process exits, or **loses the output stream** when backgrounded.
- Harness shell tabs keep streaming, stay readable via `read_shell_output` after the fact, and are visible to the user in the Harness UI.

## Tools

- `create_shell` — spawn a shell tab, optionally with a command (`zsh -ilc <command>`). Returns an id; keep it for later reads.
- `list_shells` — enumerate existing shell tabs. **Check here before spawning** — don't start a second `npm run dev` if one is already running.
- `read_shell_output` — read a shell's output (ANSI stripped). Use `match` + `context` to scan long logs for errors/warnings without pulling back megabytes.
- `kill_shell` — terminate the process AND close the tab. For natural exits the tab stays open for inspection; `kill_shell` is explicit cleanup.

## Short one-shots stay on Bash

`npm test`, `tsc --noEmit`, `git status`, `npm install` — anything that exits in a few seconds belongs on Bash. The streaming + visibility benefits of a shell tab aren't worth the tab clutter for those.

## Reading busy logs efficiently

`read_shell_output` accepts a `match` regex (case-insensitive) with a `context` line count. For a 10MB build log, pulling the whole thing wastes context — scan for `error|warn|fail` with `context: 3` instead.
31 changes: 31 additions & 0 deletions resources/plugins/harness-status/skills/harness-worktree/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
name: harness-worktree
description: Spin off an isolated Harness worktree (own git branch, own Claude session) for a new task, fix, investigation, or anything that would benefit from working in parallel. Use when the user describes a task that could be delegated, when work needs isolation from the current branch, or when the user mentions "new worktree", "split this off", "work on this in parallel", or "delegate".
---

# Worktrees in Harness

Each worktree is an independent git branch with its own terminal and Claude session. Suggest spawning one when the user wants to start something that would benefit from isolation, parallelization, or a fresh context.

## Tools

- `mcp__harness-control__create_worktree` — create a new worktree with its own Claude session. **Always provide a detailed `initialPrompt`** so the new session has full context; it won't see your conversation.
- `mcp__harness-control__list_worktrees` — list active worktrees.

## Writing a good `initialPrompt`

Brief the new session like a smart colleague who just walked into the room — they haven't seen this conversation, don't know what you've tried, don't understand why this task matters.

- **Explain what you're trying to accomplish and why.**
- Describe what you've already learned or ruled out.
- Give enough context about the surrounding problem that they can make judgment calls, not just follow narrow instructions.
- Include file paths, line numbers, and specifics — proof you understood the task.
- If you need a short response, say so explicitly.

Terse command-style prompts produce shallow, generic work.

## When NOT to spin off a worktree

- Tiny edits, single-file fixes, anything that takes less context to do than to brief.
- Tasks that genuinely need the current conversation's running state (open browser tabs, terminal output you'd lose, in-progress reasoning).
- The user said "do it here." Don't second-guess explicit scope.
Loading
Loading