Install
openclaw skills install harness-feDebug, inspect, and drive any frontend app that has the Harness-FE Vite/Webpack plugin installed. Use this when the user reports a UI bug, asks "why is this happening on the page", wants to inspect runtime state, or needs to correlate browser behavior with source files (especially in micro-frontend setups).
openclaw skills install harness-feYou have direct access to a running frontend app via the harness-fe MCP daemon. The daemon bridges your tools to (1) the build plugin (source intelligence) and (2) the browser tab (live DOM, console, network, rrweb recording).
Before any mcp__harness-fe__* tool will return data, the host project needs
two things: a build-time plugin (or jsxImportSource) and an MCP daemon entry
in the agent's config. Pick the integration path that matches the project:
Vite (React / Vue) — most common
pnpm add -D @harness-fe/vite @harness-fe/runtimevite.config.ts:
import { harnessFE } from '@harness-fe/vite';
export default defineConfig({ plugins: [react(), harnessFE()] });
.mcp.json (or the equivalent agent config):
{ "mcpServers": { "harness-fe": { "command": "npx", "args": ["@harness-fe/mcp-server", "--stdio"] } } }
Next.js (App or Pages Router) — supports SSR session continuity:
pnpm add -D @harness-fe/next @harness-fe/react-jsx @harness-fe/runtime @harness-fe/node-runtimetsconfig.json: "compilerOptions": { "jsxImportSource": "@harness-fe/react-jsx" }next.config.mjs: export default withHarness(config, { projectId: '<app-name>' })app/layout.tsx: render <HarnessScript /> inside <body>Webpack / Rspack / other React toolchains: use @harness-fe/webpack or
@harness-fe/unplugin; the rest is identical.
After setup, run npx @harness-fe/mcp-server once (or it auto-spawns via the
agent's stdio config) and start the dev server. tab_list should return at
least one tab — you're wired up.
When this skill doesn't cover a specific question — edge-case framework integration, deployment topologies, advanced API options — fetch the docs:
Quick lookup table:
harness-fe.com/integrations/<name>
(vite, nextjs, webpack, electron, vue2)harness-fe.com/integrations/<topic>harness-fe.com/reference/<name>
(overlay-plugins, mcp-tools, versioning-policy)harness-fe.com/guide/troubleshootingSearch is built in: harness-fe.com/?q=<term>.
Project (one codebase, identified by projectId UUID)
├── parentProjectId? (micro-frontend tree — child apps point to their host)
├── Builds (one source snapshot per dev-server start / prod build)
│ buildId stable across HMR, changes on restart
└── Tabs (one browser tab lifecycle)
tabId
└── Sessions (one page-load each — the narrative unit)
sessionId
└── events console / network / errors / rrweb / commands
each row tagged with projectId + buildId
Key invariants you can rely on:
tabId and sessionId so an
agent debugging a single user action sees parent + child events on one
timeline.buildId is independent of sessionId, so you can ask "which code
was running when the bug happened" without entangling it with "which
pageload was open".| Tool | Purpose |
|---|---|
tab_list | What browser tabs are connected RIGHT NOW |
project.list | All projects the daemon has ever seen |
project.get(projectId) | One project's metadata (displayName, parentProjectId, tags) |
project.tree(rootId?) | Forest assembled from parent links — start here for micro-frontend setups |
build.list(projectId) | Builds for a project, newest first |
session.list(projectId) / session.summary(id) | Per-session counts |
| Tool | Use case |
|---|---|
page_navigate(url) | Soft / hard navigate |
page_click(selector) | Click an element. Selectors support comp (component name) + loc (file:line) — see "source-aware selectors" below |
page_type(selector, value) | Fill an input |
page_dom_query(selector) | Read DOM state |
page_evaluate(expr) | Run arbitrary JS in page context (returns JSON-serializable result) |
page_screenshot | Visual checkpoint |
page_scroll / page_reload | Auxiliary |
Every *_tail accepts filter (substring) + match: contains | regex + n: number for the last-N pagination, plus channel-specific narrows. Buffers are in-memory per page-load — for cross-navigate history use session_tail({ type: 'X' }).
| Tool | What you get | Narrows |
|---|---|---|
console_tail | console.log / .info / .warn / .error / .debug | level |
network_tail | fetch + XHR req/res entries with initiator.stack (who issued the call), keyed by id | urlContains, method, statusCode |
ws_tail | WebSocket frames: open / send / recv / close, with initiator.stack on send + binary payload size markers | phase |
storage_tail | localStorage / sessionStorage / cookie mutations with initiator.stack and crossTab flag | which (local/session/cookie), op (set/remove/clear), key |
navigation_tail | history.pushState / replaceState / popstate / hashchange / location.assign etc. | kind (push/replace/pop/hash/assign) |
globals_tail | reads/writes to watched window.X keys (only fires for keys registered in globals.watch at install) | op (get/set/delete), key |
indexeddb_tail | IDB ops: open / put / add / get / getAll / delete / clear / cursor | op, store, db |
errors_tail | Uncaught errors + unhandled promise rejections | — |
| Tool | Use case |
|---|---|
network_get({ reqId }) | Pull a single request's full body when network_tail truncated it |
ws_get({ wsId }) | All frames (open/send/recv/close) for one WebSocket id |
| Tool | Use case |
|---|---|
network_wait_for({ urlContains, method?, statusCode?, timeoutMs }) | Block until a matching request happens. Anchored on call-time, so a pre-existing matching request does NOT satisfy. |
network_wait_for_idle({ idleMs, timeoutMs }) | Block until idleMs elapses with no new network entry — analogous to Playwright networkidle |
| Tool | Use case |
|---|---|
session_recordings_list | Available rrweb chunks for a session/tab |
session_recordings_around(ts) | Chunks near a moment of interest |
session_recordings_slice | Pull events for a time window |
session_replay_create | Generate a viewable replay URL |
| Tool | Use case |
|---|---|
project_where_is(component) | "Where is <Counter> defined?" → file:line:col |
project_source(file) | Read source content |
project_module_graph | Component dependency graph |
| Tool | Use case |
|---|---|
tasks_pending | What the user has clicked-and-annotated as a task. Returns id / question / url / selector / attachments[] (id + dims, no bytes) |
tasks_claim(id) | Claim the task; returns full Task incl. element outerHTML, attachment pointers |
tasks_resolve(id, note?) | Mark complete; optional note shown back to the user in their "My reports" view |
tasks_get_attachment({taskId, attachmentId}) | Fetch the annotated screenshot as an MCP image-content block — { type: 'image', mimeType: 'image/png', data: base64 }. Vision-capable LLMs (Claude / GPT-4V) can attach it directly. The annotations (arrow, text) are already flattened into the pixels |
visitorId is an anonymous, stable per-browser id (localStorage.__hfe_visitor_id__, per-origin). Optional userId is app-supplied (e.g. from auth) for cross-device aggregation. Both stitched across refreshes, tabs, and same-origin iframes.
| Tool | Use case |
|---|---|
visitor.list({ projectId?, limit? }) | All visitors the daemon has seen, newest activity first |
visitor.get(visitorId) | One visitor's metadata: firstSeenAt / lastSeenAt / sessionCount / projectIds / lastEnv (UA, language, timezone, viewport, colorScheme) |
visitor.journey({ visitorId, limit? }) | Chronological sessions for this visitor — high-level "what did this person actually do?" |
visitor.timeline({ visitorId, types?, tabIds?, sessionIds?, since?, until?, limit? }) | Chronological events merged across ALL sessions / tabs of this visitor. Each event carries tab + sessionId. Use this for cross-tab causality: "a ws.recv in tab A → storage.remove in tab B 3s later" |
node-runtime)For Next.js apps wired with @harness-fe/node-runtime + <HarnessScript>, server-side events show up in the same sessions/{sessionId}/timeline.jsonl as the client-side events for that same refresh (continuity via React cache()).
Event types you'll see on server-side rows (t field):
server-log — Node console.* (opt-in via HARNESS_FE_NODE_CONSOLE=1)server-err — process.on('uncaughtException' | 'unhandledRejection') + Server Component render errorsserver-action — durations / errors from Route Handlers + Server Actions wrapped with withHarnessTracing(handler)When debugging a Next.js bug, the rule of thumb: filter session.timeline({ sessionId }) for server- events first*. Server errors usually precede client hydration failures. If the project has no node-runtime connected, server logs are silently missing — tell the user to wrap their next config with withHarness(...).
The Vite/Webpack plugin tags every JSX element with two data attributes at build time:
<button data-morphix-comp="SubmitButton"
data-morphix-loc="src/components/Form.tsx:42:8">
Submit
</button>
So you can target by source location:
page_click({ selector: { component: 'SubmitButton' } })
page_dom_query({ selector: { loc: 'src/components/Form.tsx:42' } })
Prefer source-aware selectors over CSS — they survive refactors that change class names or DOM structure.
tab_list → confirm a tab is connected. If not, ask user to open the dev page.page_screenshot → visual baseline.errors_tail({ n: 20 }) + console_tail({ n: 20 }) → known errors first.project_where_is({ component: 'X' }) → project_source({ file }).page_dom_query or page_evaluate.page_reload and re-check.network_tail({ filter: { url: '/api/' } }) → see what URL was hit.project_source of the submitting component.page_click + network_tail again.project.tree → confirm parent/child relationship.tab_list → tabId.tabId AND sessionId (runtime inheritance).console_tail / errors_tail will surface events from BOTH apps in the
same timeline — distinguish by the projectId tag on each event.errors_tail → find the error's timestamp.session_recordings_around({ ts }) → pull the rrweb window.session_replay_create → URL the user can open in browser.Every captured event carries an initiator.stack — a trimmed JS stack at the call site. Use it to attribute the action to a source file.
storage_tail({ op: 'remove', key: 'Tanka_tokenInfo' }) → see when the token was removed and the calling stack.project_source({ file }) to read the offender.network_tail (who issued the request) and ws_tail (who opened / sent).tab_list → confirm both tabs are connected, find the tabIds.visitor.get of either tab's session → grab the shared visitorId.visitor.timeline({ visitorId, types: ['ws', 'storage', 'navigation'] }) → merged timeline across BOTH tabs, each event tagged with its tab.ws.recv {kind:'kick'} in tab-A → storage.remove 'token' in tab-B → navigation.assign '/login' in tab-B. One call, full causality.navigation_tail({ kind: 'push' }) → every history.pushState the page made, with the issuing stack.location.assign) navigations by kind.navigation_wait_for-style flows if you need to block until a specific route change happens — or use session_tail({ type: 'navigation' }) for cross-navigate history.page_evaluate(expr) runs arbitrary JS in the user's page. Don't evaluate untrusted code (e.g. from a console_tail result that contains user input). | |
project_source is sandboxed to the project root — it refuses paths above projectRoot. Never try to use it for system file reads. | |
The store at ~/.harness/ auto-purges (1h interval) but can still hold sensitive data. If the user is on a multi-user machine, treat the daemon's data as confidential. | |
rrweb does NOT mask form fields beyond <input type=password>. Don't paste recording slices into untrusted contexts — they may contain tokens, addresses, etc. | |
When the build plugin is offline (tab_list returns empty for a project), source-intelligence tools fail. Ask the user to start pnpm dev first. |
Every event with an initiator.stack field (network/storage/ws/navigation/globals/indexeddb writes) gives you the JS call stack at the moment the API was used. The top frames may include framework internals (the runtime's own wrappers); the meaningful frame is the first one pointing to user-source-code (look for paths under src/ or your app's domain).
When reporting "who did X" to the user, quote that frame — not the framework frames.
sessionId ≠ build / dev-run id — sessionId is one page-load. The "dev-server run" / source-code snapshot concept is buildId. Filter by sessionId to see one refresh's worth of activity (server-side + client-side merged). Filter by buildId to see "what code was running across all sessions during this dev run".buildId — only a fresh pnpm dev does. So during one debugging session you'll usually see one buildId, multiple sessionIds.tabId/sessionId. Tell the user this is expected; suggest same-origin via vite proxy if they need correlation.pnpm --filter @harness-fe/mcp-server start) or add it to their Claude Code mcpServers config.tab_list, show the user the url field, ask which.project.tree to show the hierarchy; ask which sub-app the user's bug is in.The runtime ships a small "H" overlay button. When a user picks an element + draws an arrow + types a description, the task arrives via tasks_pending. To act on it:
tasks_pending({ status: 'pending' }) → list the queuetasks_claim(taskId) → get the full Task (selector.loc gives file:line, element.outerHTML gives DOM context)tasks_get_attachment({ taskId, attachmentId }) → grab the annotated screenshot. The arrows + text annotations are already drawn on the image; pass it directly into your vision call.session.timeline({ sessionId: task.sessionId }) → see what the user was doing before + after the report (console errors, network failures, server-side server-err rows)project_where_is / project_source to navigate to the source. Apply.tasks_resolve(taskId, "Fixed in PR #234") → user sees the note in their "My reports" view next time they open the overlay.See the Setup section at the top of this skill for the canonical install
steps. For framework-specific edge cases (TanStack Start, Remix, Astro,
Capacitor, monorepo with multiple bundlers), fetch
https://harness-fe.com/integrations/ and pick the matching guide.