Skip to content

Commit b42cab2

Browse files
committed
docs: add OpenClaw security roadmap article
1 parent dd5c781 commit b42cab2

5 files changed

Lines changed: 197 additions & 9 deletions

File tree

114 KB
Loading

src/content.config.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import { glob } from 'astro/loaders';
33

44
const authorSchema = z.object({
55
name: z.string(),
6-
handle: z.string(),
6+
handle: z.string().optional(),
7+
url: z.string().optional(),
8+
avatar: z.string().optional(),
79
});
810

911
const blog = defineCollection({
@@ -15,6 +17,8 @@ const blog = defineCollection({
1517
// Single author (legacy)
1618
author: z.string().optional(),
1719
authorHandle: z.string().optional(),
20+
authorUrl: z.string().optional(),
21+
authorAvatar: z.string().optional(),
1822
// Multiple authors
1923
authors: z.array(authorSchema).optional(),
2024
draft: z.boolean().default(false),
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
---
2+
title: "Where OpenClaw Security Is Heading"
3+
description: "The security roadmap for making OpenClaw a powerful personal assistant runtime users can understand, observe, and trust."
4+
date: 2026-05-15
5+
authors:
6+
- name: "Jesse Merhi"
7+
url: "https://www.linkedin.com/in/jesse-merhi/"
8+
avatar: "/blog/authors/jesse-merhi.jpg"
9+
draft: false
10+
tags: ["security", "open-source", "clawhub", "plugins"]
11+
---
12+
13+
Our goal is for OpenClaw to become a trusted way to run a powerful AI personal assistant.
14+
15+
That sounds simple, but it flips how people often talk about agent runtimes. OpenClaw can read files, run commands, install plugins, talk to the network, and act on a real machine for a real user. Power like that is easy to describe as dangerous.
16+
17+
The concern is not wrong. We want OpenClaw to be powerful enough to do real work. That is the point. But powerful does not have to mean blind, unbounded, or impossible to audit.
18+
19+
This is where our security work is heading.
20+
21+
Some of this has landed. Some is rolling out. Some is still in flight. Some is research. I want to be clear about the difference, because security posts that blur those lines are usually where trust goes to die.
22+
23+
## Filesystem Handling, Runtime State, and fs-safe
24+
25+
**Status: landed for the shared filesystem primitives; in flight for the larger runtime-state move.**
26+
27+
OpenClaw runs on your machine. That means it can touch your documents, your codebases, and your photos.
28+
29+
The filesystem risk people usually reach for first is path traversal. That risk is real, but it is also only one symptom of a bigger class of bugs: unclear boundaries. Code thinks it is writing inside one root, then a symlink, absolute path, archive extraction, or sloppy join makes it cross another.
30+
31+
`fs-safe` is one answer to that. It is not a new idea bolted onto OpenClaw from the outside. It is the set of safe filesystem patterns OpenClaw had already been growing, pulled into a shared library so core code, plugins, and adjacent services can use the same root-bounded primitives.
32+
33+
It is also not a sandbox. A plugin that is allowed to run arbitrary shell commands can still do arbitrary shell-command things. We should say that plainly. `fs-safe` protects against boundary-crossing bugs in filesystem code. It does not turn untrusted code into trusted code.
34+
35+
That distinction matters. Writing inside a plugin workspace should work. Traversal and absolute-path writes outside that workspace should fail. Plugin authors should not have to reimplement those checks, and reviewers should not have to rediscover every edge case in every package.
36+
37+
The next step is making these primitives the expected pattern for plugins on ClawHub too. Bypassing them is not automatically malicious, but it is security-relevant. Over time, that kind of choice should count against a plugin's trust posture.
38+
39+
The safest filesystem call is still the one we do not make. That is the security motivation behind the in-flight SQLite runtime-state refactor. Runtime state should not sprawl across loose files when it can live in a typed database with clearer ownership, transactions, and recovery behavior.
40+
41+
Sessions, transcripts, scheduler state, and plugin state are all areas where we want less ad hoc file handling. `fs-safe` makes required filesystem access safer. Moving runtime state into a database removes whole categories of filesystem access from the runtime path.
42+
43+
## SSRF, Network Egress, and Proxyline
44+
45+
**Status: rolling out as a Node-process egress guardrail.**
46+
47+
The network problem is also a boundary problem.
48+
49+
Agentic systems make SSRF harder than it is in a normal web service. In a normal service, user-controlled URLs are often the exception. In an agent runtime, user-controlled or model-influenced URLs are normal product behavior. "Fetch this URL because someone, or something, asked for it" is not an edge case. It is a feature.
50+
51+
We started with the obvious approach: validate the URL before fetching it. Resolve DNS, check the address, then let the request go out.
52+
53+
That is not enough.
54+
55+
The validation code resolves DNS. The actual fetch resolves DNS again. Between those two moments, the answer can change. A host that pointed at a public IP during validation can point at a metadata endpoint by the time the request leaves.
56+
57+
The fix has to move closer to egress.
58+
59+
Proxyline is our Node-process routing layer for that. It is not itself the filtering proxy. It installs process-global routing for Node networking surfaces and sends traffic through the proxy you configured. The configured proxy is where the connect-time policy should live: block metadata addresses, private ranges, loopback canaries, and whatever else your environment needs blocked.
60+
61+
That split is important. Proxyline routes. The proxy enforces.
62+
63+
It also gives operators observability. If you already run a managed proxy, you can route OpenClaw through it and watch destinations, rates, and blocked attempts from infrastructure you already trust.
64+
65+
Proxyline is not a perfect cage around every possible byte. Raw sockets, native modules, unusual transports, early-captured agents, and non-OpenClaw child processes can still bypass a Node-level guardrail. We are not going to pretend otherwise. But for ordinary OpenClaw network paths, moving the control point from "a wrapper remembered to validate this URL" to "egress flows through a proxy policy" is a much better shape.
66+
67+
The validation path is simple: `example.com` should pass, a loopback canary should fail, and `openclaw proxy validate` should prove the configured route behaves that way.
68+
69+
## ClawHub Trust, ClawScan, and Plugin Provenance
70+
71+
**Status: landed for ClawHub-hosted package trust signals; still evolving for tiers and off-Hub packages.**
72+
73+
For a while, we tried to secure plugins mostly inside OpenClaw itself. That will never be enough.
74+
75+
ClawHub has to be the authority for plugin trust and provenance when a plugin comes from ClawHub. OpenClaw should consume those signals during install and update, not rely only on local inspection after the fact.
76+
77+
The ClawHub pipeline is a mix of signals: ClawScan, VirusTotal, static analysis, metadata checks, source provenance, and manual moderation. None of those is magic. Scanners are noisy in different ways, and a pipeline that screams about everything teaches users to ignore it.
78+
79+
So the hard work is calibration. Which signal is reliable? Which one false-positives? Which findings should block an install, and which should be shown as evidence without becoming a verdict?
80+
81+
That is where ClawHub can do something a local install flow cannot. It can attach trust evidence to a specific package version. It can say this release is clean, suspicious, held, quarantined, revoked, or malicious. It can block downloads for malicious or quarantined releases. It can show users what changed and why.
82+
83+
Not every OpenClaw plugin will live on ClawHub. Plugins can come from GitHub, a private registry, or a file someone sends you. That is not going away, and OpenClaw should not pretend users do not own their own machines.
84+
85+
What we can do is make the safe path better. Publish on ClawHub. Get scanned. Attach evidence. Let users weigh that evidence before install.
86+
87+
We are also exploring higher-trust tiers above the baseline: official packages, trusted publishers, and packages held to stricter review expectations. For plugins that live outside ClawHub, we want scanning to reach them too, but the exact product shape still needs work.
88+
89+
If ClawHub marks `@openclaw/kitchen-sink@0.1.0` as malicious and quarantined, the ClawHub install path should refuse it. That is the bar.
90+
91+
## Command Authorization and Prompt Fatigue
92+
93+
**Status: landed for stronger shell allowlist analysis; experimenting on contextual approval.**
94+
95+
Anyone who has used an agent harness in approval mode knows the pain.
96+
97+
Prompts arrive faster than anyone can read them. After a few minutes, users flip on YOLO mode so work can continue. At that point the prompts are not protecting anyone. They trained the user to stop reading.
98+
99+
Fixing this means fewer prompts, and better prompts.
100+
101+
The accuracy part starts with parsing. String matching is not enough. If an allowlist or blocklist only sees the outer command, wrappers become a bypass. A policy that understands `rm` but cannot see inside `bash -c "rm -rf ~/something"` is not a policy users should trust.
102+
103+
OpenClaw has been pushing on that. The shell approval path now evaluates inner command chains for common shell `-c` wrappers. If the inner chain contains an executable that is not allowed, the wrapper should not make it safe. The command highlighter also uses Tree-sitter to show users what OpenClaw found, including executables inside wrapper payloads.
104+
105+
PowerShell has its own shape and its own traps. We already fail closed for forms we do not understand, and broader PowerShell support is on the roadmap.
106+
107+
Parsing is the easier half. The harder half is deciding when to ask.
108+
109+
A static approval policy tends to do one of two bad things. It prompts on everything that might be risky, which sends users to YOLO mode. Or it relies on a fixed allow/deny list that cannot tell whether a command fits the current task.
110+
111+
The question users actually care about is simpler: did I want this to happen?
112+
113+
That is why we are experimenting with contextual approval. The goal is not "never prompt." The goal is that prompts mean something. If OpenClaw asks, the user should stop and read. If OpenClaw does not ask, that decision should be one we can defend.
114+
115+
## Static Analysis
116+
117+
**Status: landed and running on PRs.**
118+
119+
OpenClaw has had a lot of GitHub Security Advisories. That means we have patched a lot of security bugs.
120+
121+
The first job was plugging holes. The next job is making sure the same bug class does not come back.
122+
123+
After an advisory is patched, it is tempting to call it done. We should not. A GHSA is evidence about a bug class, not just one bug. The question after triage is: can we find all the code that looks like this?
124+
125+
For that, we use OpenGrep with a precise rulepack. Each rule is tied to an advisory, report, or review finding. The baseline goal is regression detection: if the same vulnerable shape returns, CI catches it before review does. The better goal is variant detection: catch nearby versions of the same mistake.
126+
127+
Precision is everything. A noisy rule is worse than no rule, because it teaches the team to ignore the channel.
128+
129+
Today the checked-in precise OpenGrep rulepack has 148 rules. It runs on PR diffs, and the full scan can be run manually. New patched advisories become candidates for new rules.
130+
131+
CodeQL covers broader ground alongside it. The challenge there is scale. OpenGrep can scan our focused rulepack quickly. CodeQL gives deeper semantic coverage but takes more time and cleanup. We need both.
132+
133+
## What This Means for OpenClaw Users
134+
135+
OpenClaw is not becoming less powerful. That would miss the point.
136+
137+
It is becoming more legible. More observable. More explicit about which boundary is being enforced where.
138+
139+
`fs-safe` does not sandbox plugins; it prevents filesystem boundary mistakes. Proxyline does not replace a filtering proxy; it routes Node egress through one. ClawHub does not remove user choice; it gives users package-version evidence before install. Command authorization does not work because prompts exist; it works when prompts are rare enough and accurate enough to matter.
140+
141+
We are not going to promise risk-free agents. Anyone promising that is selling something, or has not shipped enough yet.
142+
143+
What we can promise is the direction. OpenClaw can stay powerful while becoming more defensible. That is the runtime we want to build, and the runtime personal assistants deserve to run on.

src/pages/blog/[...slug].astro

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,30 @@ const { post } = Astro.props;
1515
const { Content } = await render(post);
1616
1717
// Normalize authors - support both legacy single author and new multi-author format
18-
type Author = { name: string; handle: string };
18+
type Author = { name: string; handle?: string; url?: string; avatar?: string };
1919
const authors: Author[] = post.data.authors
2020
? post.data.authors
21-
: post.data.author && post.data.authorHandle
22-
? [{ name: post.data.author, handle: post.data.authorHandle }]
21+
: post.data.author
22+
? [{
23+
name: post.data.author,
24+
handle: post.data.authorHandle,
25+
url: post.data.authorUrl,
26+
avatar: post.data.authorAvatar,
27+
}]
2328
: [];
2429
30+
function getAuthorAvatar(author: Author): string {
31+
return author.avatar ?? (author.handle ? `https://unavatar.io/x/${author.handle}` : '/openclaw-logo-text-dark.png');
32+
}
33+
34+
function getAuthorUrl(author: Author): string | null {
35+
return author.url ?? (author.handle ? `https://x.com/${author.handle}` : null);
36+
}
37+
38+
function getAuthorLabel(author: Author): string | null {
39+
return author.handle ? `@${author.handle}` : author.url ? new URL(author.url).hostname.replace(/^www\./, '') : null;
40+
}
41+
2542
function formatDate(date: Date): string {
2643
return date.toLocaleDateString('en-US', {
2744
year: 'numeric',
@@ -61,14 +78,16 @@ const postUrl = `https://openclaw.ai/blog/${post.id}`;
6178
{authors.map((author) => (
6279
<div class="author-info">
6380
<img
64-
src={`https://unavatar.io/x/${author.handle}`}
81+
src={getAuthorAvatar(author)}
6582
alt={author.name}
6683
class="author-avatar"
6784
loading="lazy"
6885
/>
6986
<div class="author-details">
7087
<span class="author-name">{author.name}</span>
71-
<a href={`https://x.com/${author.handle}`} class="author-handle" target="_blank" rel="noopener">@{author.handle}</a>
88+
{getAuthorUrl(author) && getAuthorLabel(author) && (
89+
<a href={getAuthorUrl(author)} class="author-handle" target="_blank" rel="noopener">{getAuthorLabel(author)}</a>
90+
)}
7291
</div>
7392
</div>
7493
))}

src/pages/blog/index.astro

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,21 @@ function estimateReadTime(content: string): string {
1919
const minutes = Math.ceil(words / wordsPerMinute);
2020
return `${minutes} min read`;
2121
}
22+
23+
type Author = { name: string; handle?: string; url?: string; avatar?: string };
24+
25+
function firstAuthor(post: (typeof posts)[number]): Author {
26+
return post.data.authors?.[0] ?? {
27+
name: post.data.author ?? 'OpenClaw Team',
28+
handle: post.data.authorHandle,
29+
url: post.data.authorUrl,
30+
avatar: post.data.authorAvatar,
31+
};
32+
}
33+
34+
function getAuthorAvatar(author: Author): string {
35+
return author.avatar ?? (author.handle ? `https://unavatar.io/x/${author.handle}` : '/openclaw-logo-text-dark.png');
36+
}
2237
---
2338

2439
<Layout title="Blog — OpenClaw">
@@ -46,13 +61,20 @@ function estimateReadTime(content: string): string {
4661
<p class="post-description">{post.data.description}</p>
4762
<div class="post-footer">
4863
<div class="post-author">
64+
{(() => {
65+
const author = firstAuthor(post);
66+
return (
67+
<>
4968
<img
50-
src={`https://unavatar.io/x/${post.data.authorHandle}`}
51-
alt={post.data.author}
69+
src={getAuthorAvatar(author)}
70+
alt={author.name}
5271
class="author-avatar"
5372
loading="lazy"
5473
/>
55-
<span class="author-name">{post.data.author}</span>
74+
<span class="author-name">{author.name}</span>
75+
</>
76+
);
77+
})()}
5678
</div>
5779
<div class="post-tags">
5880
{post.data.tags.map((tag) => (

0 commit comments

Comments
 (0)