Skip to content

Content

Content tools are how an agent reads the page. They are all typed accessors. There is no generic script execution surface. This is a deliberate constraint (OQ-2 in the design notes) so that every byte leaving the browser can be routed through the credential firewall before it reaches the client.

There are two layers of content tools:

  • Page-level tools return whole-page material as a single response. Good for “read this article” and “search this doc”.
  • Node-level tools work through a resolved node reference (@ref). Good for “click the third button in the modal” once you have located it.

Return the full readable content of the page in a structured form.

| Name | Type | Required | Description | |------|------|----------|-------------| | tab_id | string | yes | Target tab. | | format | string | no | One of markdown (default), text, html. |

{
"url": "https://example.com/article",
"title": "How browsers handle paste",
"byline": "Anna Reed",
"reading_time_min": 7,
"word_count": 1844,
"content": "# How browsers handle paste\n\n..."
}

The content field is the reader-mode extraction. The format argument controls its representation only, not what is extracted.


Return only the visible text of the page. No structure, no HTML.

| Name | Type | Required | |------|------|----------| | tab_id | string | yes |

{
"url": "https://example.com/article",
"text": "How browsers handle paste. When you paste into a web page..."
}

Useful for wc, tokenizers, embedding, or anything that expects a flat string.


Search page text with a regex and return match ranges.

| Name | Type | Required | Description | |------|------|----------|-------------| | tab_id | string | yes | Target tab. | | pattern | string | yes | RE2-compatible pattern. | | case_sensitive | boolean | no | Default: false. | | max_matches | integer | no | Cap. Default: 100. Hard maximum: 1000. |

{
"matches": [
{ "text": "paste event", "offset": 132, "line": 4 },
{ "text": "paste event", "offset": 1043, "line": 21 }
],
"total_matches": 2,
"truncated": false
}

truncated is true when the response was capped at max_matches and more matches exist.


High-level summary of the page. Useful as a cheap first call.

| Name | Type | Required | |------|------|----------| | tab_id | string | yes |

{
"url": "https://example.com/article",
"title": "How browsers handle paste",
"byline": "Anna Reed",
"excerpt": "A short summary pulled from the page metadata.",
"reading_time_min": 7,
"word_count": 1844,
"language": "en"
}

The excerpt is derived from <meta name="description">, Open Graph, or the first paragraph, in that order. It is not generated by a model.

Node-level tools operate on a reference (@ref) resolved by page_query_selector. References are opaque strings, valid for the lifetime of the current page load in the target tab, and invalidated on navigation.

Resolve a CSS selector to a node reference.

| Name | Type | Required | Description | |------|------|----------|-------------| | tab_id | string | yes | Target tab. | | selector | string | yes | Standard CSS selector. | | all | boolean | no | Return every match instead of the first. Default: false. |

{
"refs": [
{ "ref": "@ref_a1", "tag": "button", "text_preview": "Sign up" }
]
}

text_preview is a short excerpt for orientation. Do not treat it as content.


Read the visible text of a resolved node.

| Name | Type | Required | |------|------|----------| | tab_id | string | yes | | ref | string | yes |

{ "text": "Sign up to continue" }

Read an attribute of a resolved node.

| Name | Type | Required | |------|------|----------| | tab_id | string | yes | | ref | string | yes | | name | string | yes |

{ "name": "href", "value": "/signup" }

Wait until a selector resolves. Returns as soon as it does or the timeout elapses.

| Name | Type | Required | Description | |------|------|----------|-------------| | tab_id | string | yes | Target tab. | | selector | string | yes | CSS selector to wait for. | | timeout_ms | integer | no | Max wait. Default: 5000. Hard maximum: 30000. | | state | string | no | attached (default), visible, hidden. |

{ "ref": "@ref_b2", "waited_ms": 342 }

Timeout returns -32603 with data.reason = "timeout".

Every response from every content tool passes through the credential firewall’s four vectors before it lands in your client:

  1. Password fields: <input type="password"> value redacted.
  2. HTTP headers: Authorization, Cookie, Set-Cookie, Proxy-Authorization, and configured custom auth headers redacted (relevant to HAR / capture surfaces, not content tools, but the shape is consistent).
  3. Accessibility-tree autofill hints: any node with data-autofill or ARIA autocomplete fields marked sensitive is redacted.
  4. URL fragments and query parameters: access_token, id_token, refresh_token, session, and configured additional keys redacted to [REDACTED] in any URL that leaves the process.

The full rule set is on the security page. From the agent’s perspective the behavior is: values that would leak a secret come back as the literal string [REDACTED].

The design decision is that any tool that lets an agent read the page directly from JS space bypasses the firewall. There is no version of “let the agent run JavaScript” that keeps the redaction guarantee. Instead, we expose the specific accessors an agent actually needs, and every one of them is a firewall pass-through.

If a workflow needs a computation that Maho does not currently expose, the fix is to add a typed tool, not to open the JavaScript surface.