Per-user file ownership and ACL for OpenClaw. Protects skills, AGENTS.md, MEMORY.md, and configs from edits by chat users who don't have permission.
Multi-user OpenClaw bot? Anyone in the chat can ask the AI to edit any file the bot can reach — your skills, your memory, your config. This plugin adds an ownership layer: claim files with /protect, grant access with /grant @user, block everyone else. Channel-scoped identity prevents cross-platform impersonation. Pure middleware, no tools registered.
Companion to openclaw-credential-vault — same middleware pattern, different domain.
Protects sensitive files (AGENTS.md, MEMORY.md, skills, cron jobs, anything in your workspace) from being edited by chat users who don't have permission.
When a chat user asks the agent to edit a protected file, a before_tool_call hook intercepts the call and blocks it unless the requester is the owner or has been granted access. The AI never bypasses it because the hook runs in the gateway, before the tool executes.
User: "Edit AGENTS.md to add a new instruction"
↓
AI calls: edit({ file_path: "~/.openclaw/AGENTS.md", ... })
↓
file-guard hook fires:
- Extract path from params
- Look up protection for this path
- Check if requester is owner or grantee
- If not: BLOCK with explanation
↓
AI responds: "Access denied: AGENTS.md is protected. Owner: slack:U_SAUGATA.
Ask the owner to grant you access with /grant @you ..."
Without this plugin, anyone who can chat with your OpenClaw bot can ask the AI to modify any file the bot has access to. Your skills, your memory, your config — all editable by anyone in the conversation. There's no built-in OpenClaw mechanism to say "this skill belongs to Alice, only she can change it." This plugin fills that gap.
Pure middleware. No tools registered. Only:
before_tool_callhook — interceptswrite,edit,apply_patch,exec,process- Path extractor — parses target paths from each tool's params (including unified diff headers and shell command scanning)
- Path matcher — supports exact paths and glob patterns (via
minimatch) - SQLite store — tracks
protected_files,file_grants,audit_log - Channel-scoped user identity — user keys are stored as
channel:senderIdto prevent cross-channel impersonation - Mention parser — accepts
@bob,<@U_BOB>, or rawslack:U_BOBin commands
| Command | Purpose | Owner-only |
|---|---|---|
/protect <path> |
Claim ownership of a file | No |
/unprotect <path> |
Remove protection | Yes |
/grant <user> <path> [permission] |
Grant edit access | Yes |
/revoke <user> <path> |
Revoke access | Yes |
/protected |
List files you own and have access to | No |
<user> accepts:
@username— bare mention (Telegram-style or auto-converted by other channels)<@U_BOB>— Slack-style auto-mention (Slack converts@bobto this in raw text)<@123456789>/<@!123456789>— Discord-style mentionslack:U_BOB— explicit channel-scoped key (always works)
The plugin reads requesterSenderId and messageChannel from OpenClaw's runtime context. These are set by the chat platform's authenticated payload — the AI cannot fake them. User keys are stored as channel:senderId:
Slack user U_BOB → slack:U_BOB
Discord user 12345 → discord:12345
WhatsApp +880... → whatsapp:+880...
This means:
- A Slack user and a Discord user with the same handle are different identities
- Someone can't spoof your Slack ID from another channel — the channel prefix won't match
- Identity is provided by the platform, not chosen by the user or the AI
Add to ~/.openclaw/openclaw.json:
{
"plugins": {
"load": { "paths": ["/path/to/openclaw-file-guard"] },
"allow": ["file-guard"],
"entries": {
"file-guard": {
"enabled": true,
"config": {
"watchedPaths": [
"~/.openclaw/**/*.md",
"~/.openclaw/workspace/**",
"~/.openclaw/openclaw.json"
],
"alwaysProtected": [
{
"path": "~/.openclaw/openclaw.json",
"ownerUserId": "slack:U_ADMIN"
},
{
"path": "~/.openclaw/AGENTS.md",
"ownerUserId": "slack:U_ADMIN",
"grantees": ["slack:U_LEAD"]
}
]
}
}
}
}
}watchedPaths— Glob patterns defining the scope of files this plugin can guard. Files outside these patterns are unprotected. If omitted, all paths are watched (slower but safer default).alwaysProtected— Admin-pinned protections seeded on plugin startup. Each entry locks a path under a specific owner without requiring a runtime/protectcommand. Optionalgranteesarray pre-grants edit access to specific users.
git clone https://github.com/saugataroyarghya/openclaw-file-guard.git
cd openclaw-file-guard
npm install
npm run buildAdd the config to ~/.openclaw/openclaw.json, then restart OpenClaw:
openclaw gateway restartOwner editing their own file:
Saugata: "Edit AGENTS.md to add a new instruction"
AI: [calls edit, hook checks ownership, allows]
AI: "Done — added the new instruction to AGENTS.md."
Non-owner without grant:
Bob: "Edit AGENTS.md to add a new instruction"
AI: [calls edit, hook checks, blocks]
AI: "Access denied: AGENTS.md is protected. Owner: slack:U_SAUGATA.
Ask the owner to grant you access with
/grant slack:U_BOB ~/.openclaw/AGENTS.md"
Owner grants access via @mention:
Saugata: /grant @bob ~/.openclaw/AGENTS.md
Bot: "Granted edit access to slack:U_BOB on /Users/sa/.openclaw/AGENTS.md."
Bob: "Edit AGENTS.md to add a new instruction"
AI: [calls edit, hook checks, finds Bob has a grant, allows]
AI: "Done — added the new instruction to AGENTS.md."
SQLite at ~/.openclaw/file-guard/file-guard.db. Three tables:
protected_files—(agent_id, file_path, owner_user_id, protected_at)file_grants—(agent_id, file_path, grantee_user_id, granted_by, permission, granted_at)audit_log— every protect, unprotect, grant, revoke, and block, with timestamp + actor + tool name
Query the audit log to see who tried to edit what:
sqlite3 ~/.openclaw/file-guard/file-guard.db \
"SELECT datetime(timestamp/1000, 'unixepoch'), action, actor_user_id, target_path, tool_name FROM audit_log ORDER BY timestamp DESC LIMIT 20"This plugin is defense in depth, not a security boundary.
- Not OS-level enforcement. The OpenClaw process can still read/write any file it has filesystem permissions for. This plugin blocks the AI from doing edits via tool calls. For OS-level enforcement, run OpenClaw as a dedicated user and use
chmod/chownon protected files. - Not encryption. Files aren't encrypted at rest. The plugin tracks ownership metadata only.
- Not read protection. Anyone can still
readorcata protected file. The plugin only gates write operations. - Not anti-jailbreak for arbitrary
exec. A determined AI could try clever shell tricks (base64-encoded commands, indirect file paths via variables). Our scanner catches obvious cases. For real protection against this, OpenClaw's sandboxed exec mode prevents the AI from accessing the filesystem at all when running under the sandbox.
The right model: this plugin + OS permissions + sandboxed exec = layered protection.
| Aspect | credential-vault | file-guard |
|---|---|---|
| Domain | API authentication | File access control |
| Hook | before_tool_call (gate + inject) |
before_tool_call (gate only) |
| Storage | Encrypted credentials (AES-GCM) | Ownership metadata (plaintext) |
| Tools registered | vault_fetch |
None |
| Commands | /connect, /disconnect, /connections |
/protect, /unprotect, /grant, /revoke, /protected |
| User identity | channel:senderId (anti-spoofing) |
channel:senderId (anti-spoofing) |
You can run them together. They don't conflict.
MIT