| name | harness-fe |
| description | Debug, 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, drive an automated browser test, or needs to correlate
browser behavior with source files (especially in micro-frontend setups).
|
| allowed-tools | ["mcp__harness-fe__*","mcp__harness-solo__*","Read","Grep","Bash"] |
Harness-FE Agent Skill
You 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).
Tool names use underscores. Every tool is mcp__<server>__<name> with
underscores throughout โ project_list, session_summary, page_click. The
server prefix depends on the user's MCP config (commonly harness-fe for the
shared gateway, harness-solo for the loopback daemon). This doc writes the
bare name (project_list); prepend whichever prefix your environment exposes.
Setup โ do this first if the project isn't wired up
Before any 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/runtime
- Add to
vite.config.ts:
import { harnessFE } from '@harness-fe/vite';
export default defineConfig({ plugins: [react(), harnessFE()] });
- Register the daemon in
.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-runtime
tsconfig.json: "compilerOptions": { "jsxImportSource": "@harness-fe/react-jsx" }
next.config.mjs: export default withHarness(config, { projectId: '<app-name>' })
app/layout.tsx: render <HarnessScript /> inside <body>
- Same MCP daemon config as above.
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.
Documentation โ fetch when you need depth
When this skill doesn't cover a specific question โ edge-case framework
integration, deployment topologies, advanced API options โ fetch the docs:
Quick lookup table:
- Framework-specific setup โ
harness-fe.com/integrations/<name>
(vite, nextjs, webpack, electron, vue2)
- LAN / Docker / multi-daemon โ
harness-fe.com/integrations/<topic>
- Full API reference โ
harness-fe.com/reference/<name>
(overlay-plugins, mcp-tools, versioning-policy)
- Troubleshooting flowcharts โ
harness-fe.com/guide/troubleshooting
Search is built in: harness-fe.com/?q=<term>.
Mental model
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:
- Same-origin iframes inherit the parent's
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".
- The runtime auto-disables in production builds โ anything you see here is
dev-time only.
Solo vs Team mode
How you reach the daemon changes what you can see and do โ read this before
concluding "nothing is there" or "the tool is broken".
Solo (default โ loopback, zero config): the agent spawns @harness-fe/dev-cli
over stdio; the daemon is fully trusted. You see every project / session /
task, page_* runs immediately (no approval), and the whole tool catalog is
available.
Team (via the gateway): the agent connects over HTTP-MCP to
@harness-fe/gateway with a scoped token; one daemon is shared by many apps, so
access is governed:
| Behaviour | Solo | Team (gateway) |
|---|
| Visible projects / sessions / tasks | all | only the projects your token is bound to โ others return empty [] (isolation, not a bug) |
page_* (click/type/navigate/โฆ) | runs immediately | needs Browser Consent โ the user approves in-page first; a denial returns ok:false / CONSENT_DENIED |
Available tools (tools/list) | full catalog | scope-filtered โ a read-only token never sees page_*; calling one is denied (-32001 scope denied) |
| Transport | stdio (direct) | HTTP-MCP via the gateway (routed + audited) |
In team mode, adjust your behaviour:
- An empty
project_list / session_list / tasks_pending most likely means
your token isn't bound to that project โ not that the app is broken. Say
you may lack access rather than asserting nothing exists.
- Before a
page_* action, expect a consent prompt to the user. If it's denied,
report it and don't blindly retry.
- If an expected tool is missing, your token's scope doesn't include it
(
read vs read+control). write is for the browser runtime only โ never an agent.
In solo mode none of these gates apply โ proceed directly. (Tool notes below
flag the team-mode differences with [team].)
Tool catalog
Identity & topology
| 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 |
project_sessions(projectId) | Sessions belonging to a project |
build_list(projectId) / build_get(buildId) | Builds for a project (newest first) / one build's detail |
session_list(projectId) / session_summary(sessionId) | Per-session counts |
dashboard_open | Open the Harness web dashboard for visual inspection |
experimental_ping | Liveness check โ confirm the daemon is reachable |
Page interaction (drive the browser)
[team] Everything here is control scope: hidden from a read-only token's tools/list, and each call triggers a Browser Consent prompt the user must approve before it runs (a denial returns ok:false / CONSENT_DENIED). In solo mode they run directly.
| Tool | Signature | Use case |
|---|
page_navigate({ url }) | url | Soft / hard navigate |
page_click({ selector }) | selector | Click an element |
page_type({ selector, value }) | selector + value | Type into an input (per-key events) |
page_paste({ selector, content, html? }) | selector + content | Paste text/HTML in one shot (e.g. rich editors) |
page_select({ selector, value }) | selector + value | Choose a <select> option, fires change/input |
page_check({ selector, checked }) | selector + bool | Set a checkbox/radio state |
page_upload({ selector, files }) | selector + [{name, content(base64), mimeType?}] | Set files on <input type=file> |
page_dom_query({ selector }) | selector | Read DOM state (outerHTML, attrs, text) |
page_evaluate({ expr }) | JS expr | Run arbitrary JS in page context (returns JSON-serializable result) |
page_wait_for({ predicate, timeoutMs? }) | "network.idle" | "dom.ready" | JS expr | Block until a condition is truthy โ use before acting on async/late-rendered UI |
page_set_dialog_handler({ type, value }) | alert|confirm|prompt + return value | Pre-arm the answer to the next native dialog so it doesn't block your action |
page_screenshot | โ | Visual checkpoint |
page_scroll / page_reload / page_set_html / page_set_style | โ | Auxiliary |
Every page action takes an optional tabId (from tab_list); omit it to target
the most-recent active tab. See Selectors below for the selector object shape.
Telemetry tail
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({ sessionId, 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 | โ |
Session timeline (cross-navigate history)
*_tail buffers reset on each page-load. To see the whole narrative of a session
(client + server events merged), use these:
| Tool | Use case |
|---|
session_tail({ sessionId, type?, n?, since?, until? }) | Last-N events from the full session timeline. type filters one or many event types ('err', ['ws','storage'], โฆ) |
session_search({ sessionId, query, type?, limit? }) | Substring search across a session's events |
session_summary({ sessionId }) | Per-type event counts for a session |
Targeted fetch / single entry
| 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 |
Wait for the page to do something
| Tool | Use case |
|---|
page_wait_for({ predicate, timeoutMs? }) | Block until predicate is truthy. Built-ins "network.idle" / "dom.ready", else any JS expression (e.g. "document.querySelector('[data-testid=done]')") |
network_wait_for({ urlContains?, urlRegex?, method?, statusCode?, timeoutMs? }) | Block until a matching request happens. Anchored on call-time โ a request that already fired does NOT satisfy it. |
network_wait_for_idle({ idleMs, timeoutMs }) | Block until idleMs elapses with no new network entry โ analogous to Playwright networkidle |
Replay & forensics
| 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 |
Source intelligence (bridge browser โ code)
| 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 |
Annotation tasks (human โ agent handoff)
[team] tasks_pending only returns tasks for the projects your token is bound to โ an empty list may mean "not my project", not "no tasks".
| 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?, resolution?) | Mark complete. note is shown back to the user in "My reports". resolution (P7) closes the loop: { type, commit, prUrl, verificationSessionId } โ back-links the report to its fix + the re-test that proved it. verifiedAt defaults when a verificationSessionId is given |
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 can attach it directly. The annotations (arrow, text) are already flattened into the pixels |
Visitor identity & user journey
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" |
Server-side capture (Next.js, role = node-runtime)
For Next.js apps wired with @harness-fe/node-runtime + <HarnessScript>, server-side events show up in the same session timeline 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 errors
server-action โ durations / errors from Route Handlers + Server Actions wrapped with withHarnessTracing(handler)
When debugging a Next.js bug, the rule of thumb: session_tail({ sessionId, type: ['server-log','server-err','server-action'] }) 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(...).
Selectors โ how to target an element
Every page_* action that touches an element takes a selector object (not a
bare string). Provide one or more fields; pick the most robust available:
| Field | Targets by | When to use |
|---|
component | React component name (data-morphix-comp) | refactor-proof; first choice |
file + line | source location (data-morphix-loc) | pin one specific instance in code |
text | visible text content | buttons / links with stable copy |
role / ariaLabel | ARIA role / aria-label | accessible, semantic targeting |
css | raw CSS selector | escape hatch โ incl. data-testid (below) |
nth | 0-based index among matches | disambiguate when a selector matches several elements |
The Vite/Webpack plugin tags every JSX element at build time, which is what
component / file / line read:
<button data-morphix-comp="SubmitButton"
data-morphix-loc="src/components/Form.tsx:42:8">
Submit
</button>
Example calls:
page_click({ selector: { component: 'SubmitButton' } })
page_click({ selector: { text: 'Pay now' } })
page_dom_query({ selector: { file: 'src/components/Form.tsx', line: 42 } })
page_click({ selector: { component: 'TodoItem', nth: 2 } })
Prefer component / text / role over css โ they survive refactors that
change class names or DOM structure. Use nth to break ties instead of writing a
brittle deep CSS path.
Add your own stable hooks while coding
When you're already editing a component for a feature or a fix, proactively
add a data-testid to the elements you (or the user) will want to drive or
assert on later โ it's an explicit, refactor-proof handle that complements the
auto-injected component/file+line:
<button data-testid="checkout-submit">Pay now</button>
Then target it via the css field:
page_click({ selector: { css: '[data-testid="checkout-submit"]' } })
page_dom_query({ selector: { css: '[data-testid="order-total"]' } })
Guidelines:
- Add
data-testid to interaction targets (buttons, inputs, links) and
assertion anchors (status text, totals, list rows) โ the things a test
actually touches, not every node.
- Use a stable, intent-describing name (
checkout-submit, not btn-3).
- Match the project's existing convention if one exists (
data-testid vs
data-test vs data-cy) โ grep before inventing one.
A testid you add today makes the next "drive the browser and verify" loop
deterministic instead of fragile.
Decision flows
Flow 1: User reports a visual bug
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.
- If errors implicate a component:
project_where_is({ component: 'X' }) โ project_source({ file }).
- Form a hypothesis. Verify with
page_dom_query or page_evaluate.
- Suggest a fix in source. Use Edit. Then
page_reload and re-check.
Flow 2: User reports "the form submits to wrong endpoint"
network_tail({ urlContains: '/api/' }) โ see what URL was hit.
- Compare with
project_source of the submitting component.
- Confirm with
page_click + network_tail again.
Flow 3: Micro-frontend bug ("the iframe child app errored")
project_tree โ confirm parent/child relationship.
tab_list โ tabId.
- Note: parent + child share
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.
Flow 4: "What happened just before the crash"
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.
Flow 5: "Who deleted my login token?" / "Who issued this fetch?"
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.
- The stack's first user-code frame names the file + line.
project_source({ file }) to read the offender.
- Same approach works for
network_tail (who issued the request) and ws_tail (who opened / sent).
Flow 6: Cross-tab bug ("opening tab B kicks me out of tab A")
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.
- Sequence: e.g.
ws.recv {kind:'kick'} in tab-A โ storage.remove 'token' in tab-B โ navigation.assign '/login' in tab-B. One call, full causality.
Flow 7: Track SPA route changes
navigation_tail({ kind: 'push' }) โ every history.pushState the page made, with the issuing stack.
- Distinguish SDK-driven (react-router) vs explicit (
location.assign) navigations by kind.
- For cross-navigate history use
session_tail({ sessionId, type: 'navigation' }).
Flow 8: Proactively drive the browser to test a change
You don't have to wait to be asked "go test this." Once you've written or
changed UI code and a dev tab is connected, driving the app yourself is
usually the fastest way to know whether the change actually works โ faster
and more honest than reasoning about it from the source alone. Reach for this
after implementing a feature, fixing a UI bug, or to confirm a flow end-to-end.
tab_list โ is a tab connected? If not, ask the user to open the dev page
(or start pnpm dev). [team] expect a Browser Consent prompt before the
first page_* action โ tell the user it's coming.
page_navigate({ url }) to the route, then page_wait_for({ predicate: 'dom.ready' })
(or a JS predicate for the element you need) so you don't act before it renders.
- Drive the actual user flow with
page_click / page_type / page_select /
page_check, targeting data-testid (via css) or component selectors.
If the flow pops a native confirm/alert, pre-arm page_set_dialog_handler.
- Assert the outcome โ combine
page_dom_query / page_evaluate for state,
errors_tail + console_tail for regressions, and network_wait_for /
network_tail for the right request/response. page_screenshot for a visual
checkpoint.
- Report what you observed (what you clicked, what you saw), not just "it
should work." If it failed, the error/network evidence is already in hand.
Do it without being asked after a UI change or bug fix (reproduce โ fix โ
re-drive to prove clean), or before claiming something is done. Ask first for
flows with side effects (payments, sending messages, destructive actions) โ and
in team mode respect a CONSENT_DENIED instead of retrying.
Flow 9: User filed a task via the in-page overlay
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 queue
tasks_claim(taskId) โ get the full Task (selector 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_tail({ sessionId: task.sessionId }) โ see what the user was doing before + after the report (console errors, network failures, server-side server-err rows)
- Form a fix. Use
project_where_is / project_source to navigate to the source. Apply the edit + commit (host git/gh tooling โ writeback lives outside harness).
- Verify the fix โ close the loop. Re-drive the reported flow against the patched build:
session_replay_create({ sessionId: task.sessionId }) to recall the exact steps the user took, reproduce them with page_*, then prove it's clean โ errors_tail / session_tail({ sessionId, type: 'err' }) show no new errors and page_* confirms the expected behavior. Keep the new sessionId of this re-test.
tasks_resolve(taskId, "Fixed in PR #234", { type: 'code-fix', commit: '<sha>', prUrl: '<url>', verificationSessionId: '<re-test session>' }) โ the structured resolution back-links report โ fix โ proof; the user still sees note in their "My reports".
If you can't reproduce or decide not to fix, still resolve with the reason so the loop is closed: resolution: { type: 'cannot-reproduce' } (or 'wontfix' / 'duplicate').
Troubleshooting
When a page_* action fails
| Symptom | Likely cause | Do this |
|---|
| element not found | selector too specific, or element not rendered yet | page_dom_query to confirm it exists; if async, page_wait_for first |
| matched the wrong one | ambiguous selector | add nth, or switch to component / text / file+line |
| action ran, nothing happened | the handler errored | errors_tail + console_tail; check the expected network_tail entry fired |
network_wait_for times out | the request fired BEFORE the wait โ it only sees requests after the call | call network_wait_for first, THEN trigger the action |
| a native dialog blocks the action | unanswered alert / confirm / prompt | page_set_dialog_handler BEFORE the action that triggers it |
page_evaluate returns nothing useful | result isn't JSON-serializable (DOM node, function, circular) | return a primitive/plain object (el.textContent, {...}) instead |
[team] CONSENT_DENIED / -32001 scope denied | user declined, or token lacks control scope | report it; don't retry blindly โ ask the user to approve or widen scope |
When you "see nothing"
| Symptom | Likely cause | Do this |
|---|
tab_list empty | no dev tab open, or dev server down | ask the user to open the page / start pnpm dev |
source tools fail (project_where_is, project_source) | build plugin offline | ask the user to start pnpm dev (the plugin runs in the bundler) |
project_list / session_list / tasks_pending empty [team] | your token isn't bound to that project | say you may lack access โ don't assert nothing exists |
| daemon unreachable | MCP server not configured / not running | experimental_ping; if it fails, re-check the MCP config (see Setup) |
| server-side (Next.js) events missing | no @harness-fe/node-runtime connected | tell the user to wrap next config with withHarness(...) |
Production deployment options
Hiding the overlay (data capture unaffected)
The in-page floating "H" button is off by default in production โ set overlay: false
in the plugin config. All rrweb recording and event reporting continue unchanged.
harnessFE({ projectId: 'xxx', overlay: false })
withHarness(config, { overlay: false })
A hidden overlay can still be toggled manually by pressing Cmd/Ctrl + Shift + H
(only works when overlay: true โ the DOM must be present).
Control commands (page_click, page_type, โฆ)
The consent option controls whether MCP agents can drive the page:
| Value | Behaviour |
|---|
'deny' (default) | All control commands rejected immediately โ no prompt, no UI |
'session' | First control command prompts the user once per page-load |
'always' | Prompt before every control command |
'off' | Run freely, no prompt (loopback dev only) |
Set via plugin config (takes priority over gateway):
harnessFE({ projectId: 'xxx', consent: 'deny' })
Or per gateway (daemon side):
harness serve --governed # โ consent: 'session' for all peers
page_evaluate (arbitrary JS) always prompts, regardless of consent mode.
Storage cap
Default: 1 GiB hard cap on the data directory. When exceeded, oldest sessions
are evicted automatically during the hourly purge. Override with:
docker run -e HARNESS_MAX_STORAGE_BYTES=2147483648 morphixai/harness-fe
HARNESS_MAX_STORAGE_BYTES=2147483648 harness serve
HARNESS_MAX_STORAGE_BYTES=0 harness serve
Visitor identity
visitorId โ anonymous, auto-generated, stored in localStorage. Not configurable by the plugin.
userId โ app-supplied identity (supabase uid, auth0 sub, โฆ). Pass via plugin config or <HarnessScript userId={user.id} />.
Constraints & safety
| |
|---|
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, 1 GiB cap) 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. | |
consent: 'deny' is the default โ if page_* tools return CONSENT_DENIED, the deployment intentionally blocks control. Do not instruct the user to "just disable consent" without understanding the security intent. | |
Reading initiator stacks
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.
Common gotchas
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".
- HMR doesn't change
buildId โ only a fresh pnpm dev does. So during one debugging session you'll usually see one buildId, multiple sessionIds.
network_wait_for is call-time-anchored โ it ignores requests that already fired. Set up the wait BEFORE the action that triggers the request.
- Cross-origin iframe โ identity inheritance silently degrades. Child gets its own
tabId/sessionId. Tell the user this is expected; suggest same-origin via vite proxy if they need correlation.
When to ask for clarification
- "There's no MCP daemon running" โ user needs to start it (
pnpm --filter @harness-fe/mcp-server start) or add it to their Claude Code mcpServers config.
- "Multiple tabs are connected, which one?" โ call
tab_list, show the user the url field, ask which.
- "Multiple projects share this tabId" โ common in micro-frontends. Use
project_tree to show the hierarchy; ask which sub-app the user's bug is in.
Wire-up details
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.