Fruitful Docs
GuidesCapture

Write capture hooks

Run sandboxed code at lifecycle points during capture, with exactly the page operations you declare.

Most routes need no code: readiness, scrolling, and login cover the common cases. When a page needs a click to reveal its content, or a modal dismissed, a capture hook does it. Hooks run in a sandbox and can only perform the page operations their declared capabilities grant.

Declare the module and its exports

"hooks": {
  "file": "linkedin-capture-hooks.js",
  "exports": [
    { "name": "prepareCompanyPosts", "capabilities": ["browser.dom.read", "browser.dom.click"] }
  ]
}
  • file is the bundled module, listed in the manifest's files.
  • Every hook a route uses must appear in exports with its capabilities. The list becomes the sandbox import table: an operation you did not declare does not exist inside the hook.
  • Publish validation checks each name is exported by the module.

Capabilities

CapabilityPage operations
browser.dom.readwaitForVisible, count
browser.dom.clickclick
browser.dom.typetype (refuses password fields and credential-looking input)

There is no navigation, no network, and no way to read cookies. Each operation is clamped by a timeout.

Attach it to a lifecycle point

{
  "id": "company-posts",
  "kind": "collection",
  "login": "linkedin",
  "ready": { "selector": "[data-view-name=\"feed-full-update\"]" },
  "on": { "afterReady": { "hook": "prepareCompanyPosts", "onError": "continue" } },
  "scroll": { "kind": "finite", "maxAttempts": 2 },
  "binding": "company-posts"
}

Three points exist: afterLoad (before the readiness wait), afterReady (after it, before scrolling), and beforeSnapshot (after scrolling settles). onError is per attachment: continue proceeds to the snapshot, fail aborts the capture. The same hook can be load-bearing on one route and best-effort on another.

Write the hook

The module is TypeScript under authoring/src/, bundled by plugin generate. A hook is a function that receives the page API:

export async function prepareCompanyPosts(page: CaptureApi): Promise<void> {
  const sortMenu = 'button[aria-label="Sort order"]';
  if ((await page.count(sortMenu)) === 0) return;
  await page.click(sortMenu);
  await page.waitForVisible('[role="menuitem"]');
  await page.click('[role="menuitem"]:nth-of-type(2)');
}

Write against CaptureApi from workspace.packages.universal.lib; it is the whole vocabulary. The bundle must be self-contained: a node: import fails plugin generate. Declare stdlib on the hooks block if you need URL, Intl, or base64; see Plugin runtime environment.

Reference: captureHooks, hookExport, lifecycle, hookRef.

On this page