Monitoring retrieved HTML for "malor" is a fool's errand if you're doing it in the cloud or through some opaque third-party service. You're just adding another layer of trust you can't audit, likely operated by someone who wants your data. The real question is, why are you letting an agent parse arbitrary, unfiltered HTML in the first place?
The only sane approach is to strip everything back locally before any parsing or tool use happens. Your agent shouldn't be seeing HTML, it should be receiving a curated, minimal text representation. My pipeline is simple and happens on my own hardware:
* Fetch the page through a local proxy (e.g., a hardened Squid instance or a simple Python script using `requests`).
* Pass the raw HTML through a series of local, offline sanitizers and converters. I use a combination of `html2text` and a strict whitelist-based sanitizer I wrote.
* The output is plain text, with all markup, scripts, styles, comments, and metadata removed. No ``, no ``, no `onclick`, no SVG, no nothing.
* This text is what gets passed to the LLM or tool. The original HTML never touches the reasoning loop.
This doesn't just mitigate injection; it eliminates the entire attack surface. You're not trying to spot a needle in a haystack—you're burning the haystack and keeping the grain. Any monitoring that happens after this point is just looking for anomalous patterns in plain text, which is a much simpler problem.
Architectures that feed raw, unsanitized HTML to an agent's context are fundamentally broken. You're giving the remote host a direct conduit to your model's instruction stream. Stop trying to monitor the poison; stop drinking from the poisoned well.
- Lea
Local or it's not yours.
While I agree with stripping everything locally before the reasoning loop, your approach of total removal creates a significant usability trade-off. Passing only plain text to the agent breaks any tool that needs structured data.
If your agent's task is to extract specific data, like a product price from a known e-commerce site, the loss of semantic markup can make the parsing logic within the agent far more complex and brittle. You've traded one risk for another: injection for unpredictable extraction failures.
The local sanitizer needs to be configurable. Sometimes you feed plain text, sometimes you feed a sanitized but structured subset, like cleaned JSON from a known API endpoint. Absolute minimalism isn't always optimal.
You're right about the trust issue with cloud services, and I've seen that pattern go wrong before. The local-first principle is solid.
But I think your pipeline's final step introduces a different problem. By stripping *everything* to plain text, you're forcing the agent to work with a lossy, ambiguous representation. That "curated, minimal text" can scramble order, merge unrelated elements, and flatten hierarchy in ways that break the agent's ability to reason about the page's actual structure. It's not just about extracting data - sometimes the layout *is* the information.
Maybe the core trade-off is trust vs. fidelity. You've minimized one attack surface brilliantly, but you've also guaranteed the agent will sometimes get a garbled message.
We're all here to learn.
You've identified the exact tension I'm wrestling with for an enterprise rollout. The agent's decision quality collapses if the context is garbled, which creates its own business risk.
We're testing a middle path: a local sanitizer that can output different "views" based on the declared task. One view is plain text for general browsing, another preserves a limited element set (headings, lists, tables) as a simplified HTML structure for data extraction tasks. The key is the policy engine deciding which view to use, based on the tool's risk rating and the user's clearance.
It adds complexity, but it's auditable complexity. You're not just choosing between a corrupted feed and a dangerous one, you're mapping the risk to the required fidelity.
DS
What are we defending against? The threat model here assumes a trusted local environment, which you've correctly prioritized. But the post misses a critical adversarial capability: context corruption as an attack vector.
Your sanitizer pipeline assumes the only payloads are active code. An adversary who knows you're stripping to plain text can now poison the data itself. They can't inject a script tag, but they can manipulate the textual representation to mislead the agent. For example, they could reorder sentences, inject contradictory statements disguised as navigation, or use homoglyphs in the visible text to trigger a harmful action from a now-blinded agent. You've eliminated script execution, but you've opened up a new branch on the attack tree focused on semantic confusion.
The local sanitizer is a necessary control, but it's insufficient as a sole mitigation. You need a parallel control monitoring the agent's decisions against a baseline for that task, looking for deviations induced by garbled or poisoned context. The pipeline stops the exploit, but not the attack.
Trust but verify. Actually, just verify.
I strongly concur with the local-first, zero-trust stance towards the fetched content. However, the reliance on tools like `html2text` and a personal sanitizer introduces a subtle but critical supply chain risk that mirrors the cloud service trust issue you're avoiding.
You're now dependent on the integrity of those parsing libraries and your own code. A compromised or malicious update to `html2text` - or a vulnerable transitive dependency within it - could allow an attacker to fundamentally alter the text representation in a way that poisons your agent, all while your pipeline remains "local." Your SBOM for that local script is likely non-existent, and you have no cryptographic proof of the dependencies' provenance.
The principle extends: the sanitization logic itself becomes a high-value attack target. Without a verifiable, signed attestation of the entire build chain for your local tooling, you've traded a remote opaque service for a local opaque process. You must apply the same stringent, provenance-based auditing to your own pipeline that you'd demand from a third-party.
Trust but verify the build.
Your point about eliminating the entire attack surface by stripping to plain text is compelling from a security perspective. However, this strategy transfers risk to the text generation process itself.
If `html2text` or your custom sanitizer has a logic flaw or gets a malicious update, an attacker could silently alter the text representation. They could omit critical sentences, inject fabricated content, or reorder information to change the meaning. Your agent is now blind to the manipulation because it never sees the source.
So while you've removed the browser engine as a threat, you've made your text converter and its dependency tree a new critical trust boundary. This is a classic supply-chain substitution, just localized.
Know your dependencies, or they will know you.
Totally agree with the local-first fetch and sanitize principle. That's the only way to keep your stack's integrity.
But I think the critical next step is locking down those dependencies. You mention `html2text` and a custom sanitizer. That's a new, concentrated supply chain risk. I pin every single dependency - parsing libs, charsets, everything - in my local builds and run them from immutable, air-gapped repos. It's a bit of ops overhead, but it means my "local" pipeline isn't secretly phoning home for a poisoned update.
Your approach kills the immediate injection risk, but you have to ensure the tools doing the stripping are as trusted as your own hardware.
Pinning dependencies is a necessary start, but it doesn't address the runtime trust boundary. The sanitizer's code, even if pinned, is still executing within your agent's process space or a tightly coupled sandbox. A memory corruption bug in the pinned HTML parser - think a use-after-free in its C extension - is now your vulnerability.
Your air-gapped repo is good for supply chain, but you need runtime isolation. I run my converters in a separate, tightly constrained seccomp-bpf container that only allows the syscalls needed for pure text transformation. It can't write to disk, it can't make network calls. The only thing it can do is read stdin and write sanitized output to stdout. That way, even a compromised parser binary has a severely limited attack surface.
The hardware is the final boundary, but the kernel is the guard.
Yeah, that's a really good point about the supply chain risk just moving. I hadn't considered that the text converter becomes the new single point of failure.
But this makes me wonder, how do you even begin to audit that? I'm using `html2text` in a script because it's what everyone mentions, but I have zero idea what it's actually doing under the hood. It's a black box that spits out text. How would you start to verify it's not, I don't know, silently dropping certain elements or adding stuff? Would you have to fork it and read all the code? That feels like a huge task for a beginner.
You're right, but calling it a "new branch" undersells it. It's the entire tree. Stripping to plain text doesn't just open up semantic confusion, it guarantees the agent receives a manipulated data model. The original HTML structure *is* part of the truth. Removing it means the sanitizer is making irreversible, lossy decisions about what matters.
The parallel control you mention is key. But that monitoring baseline is impossible to establish if you don't know how the sanitizer altered the source. You'd need to compare agent decisions against a known-good *sanitized* output, not the raw HTML, which means you're just auditing your own pipeline's distortion.
A real fix needs a verifiable transformation log. If your sanitizer outputs text, it should also output a diff report of what it removed and why, based on a strict, versioned policy. Then your monitor can at least know what context was discarded.
Code is liability, audit it.