Access Control
Layered permission model: users belong to groups, groups define defaults, banks override per-group or per-user.
Directory Structure
Access control uses a self-contained directory at .openclaw/hindsight/:
.openclaw/hindsight/
├── config.json5 <- plugin settings
├── banks/
│ ├── agent-1.json5 <- bank config (file name = agent ID)
│ ├── agent-1/ <- $include fragments
│ └── ...
├── groups/
│ ├── _default.json5 <- REQUIRED — anonymous/unknown users
│ ├── group-1.json5
│ ├── group-2.json5
│ └── ...
└── users/
├── user-1.json5 <- canonical ID = file name
└── ...
Enable by setting configPath in your plugin config:
"hindclaw": {
"enabled": true,
"configPath": "./hindsight"
}
Users
A user file defines identity only — who they are across channels. No permissions, no group membership.
// users/user-1.json5
{
"displayName": "Alice",
"email": "[email protected]",
"channels": {
"telegram": "123456",
"slack": "U123456"
}
}
Groups
A group file defines who's in it and what permission defaults they get.
Role Groups
// groups/group-1.json5
{
"displayName": "Executive",
"members": ["user-1"],
"recall": true,
"retain": true,
"retainRoles": ["user", "assistant", "tool"],
"retainTags": ["role:executive"],
"recallBudget": "high",
"recallMaxTokens": 2048,
"recallTagGroups": null, // no filter — sees everything
"llmModel": "claude-sonnet-4-5-20250929"
}
// groups/group-2.json5
{
"displayName": "Staff",
"members": ["user-3"],
"recall": true,
"retain": true,
"retainRoles": ["assistant"],
"retainTags": ["role:staff"],
"retainEveryNTurns": 2,
"recallBudget": "low",
"recallMaxTokens": 512,
"recallTagGroups": [
{"not": {"tags": ["sensitivity:confidential", "sensitivity:restricted"], "match": "any_strict"}}
],
"llmProvider": "openai",
"llmModel": "gpt-4o-mini"
}
Department Groups
// groups/group-3.json5
{
"displayName": "Sales Team",
"members": ["user-2", "user-3"],
"recallTagGroups": [
{"tags": ["department:sales"], "match": "any"}
],
"retainTags": ["department:sales"]
}
Anonymous Fallback (required)
// groups/_default.json5
{
"displayName": "Anonymous",
"members": [],
"recall": false,
"retain": false
}
Group Fields
| Field | Type | Description |
|---|---|---|
displayName | string | Human-readable name |
members | string[] | Canonical user IDs (file names from users/) |
recall | boolean | Can read from memory |
retain | boolean | Can write to memory |
retainRoles | string[] | Message roles retained: user, assistant, system, tool |
retainTags | string[] | Tags added to all retained facts |
retainEveryNTurns | number | Retain every Nth turn |
recallBudget | string | Recall effort: low, mid, high |
recallMaxTokens | number | Max tokens injected per turn |
recallTagGroups | TagGroup[] or null | Tag filter for recall. null = no filter. |
llmModel | string | LLM model for extraction |
llmProvider | string | LLM provider for extraction |
excludeProviders | string[] | Skip these message providers |
Merge Rules (multiple groups)
When a user belongs to multiple groups:
| Field | Rule |
|---|---|
recall, retain | Most permissive wins (true > false) |
retainRoles, retainTags | Unioned |
recallBudget | Most permissive (high > mid > low) |
recallMaxTokens | Highest value wins |
recallTagGroups | AND-ed together |
llmModel, llmProvider | Alphabetically first group that defines it wins |
retainEveryNTurns | Lowest value wins (most frequent) |
excludeProviders | Unioned (most restrictive) |
Bank-Level Permissions
Each bank can override group defaults — the most specific scope wins:
// In bank config
{
"permissions": {
"groups": {
"group-1": { "recall": true, "retain": true },
"group-2": { "recall": true, "retain": false },
"_default": { "recall": false, "retain": false }
},
"users": {
"user-2": { "recallBudget": "high", "recallMaxTokens": 2048 }
}
}
}
Resolution Algorithm
Per-field, most specific scope wins:
- Merge global groups — collect all user's groups, merge with rules above
- Bank
_defaultbaseline — if bank haspermissions.groups._default, start there - Bank group overlay — merge bank-level group entries for this user's groups
- Bank user override — apply per-user override if defined
Banks without permissions fall through to global group defaults (backward compatible).
Tag-Based Filtering
recallTagGroups uses Hindsight's tag_groups API for boolean filtering:
// Exclude restricted content
{"not": {"tags": ["sensitivity:restricted"], "match": "any_strict"}}
// Include only department content (plus untagged)
{"tags": ["department:sales"], "match": "any"}
Tags come from two sources:
- Code-level —
retainTagsfrom groups + autouser:<id>tag - LLM-extracted — entity labels with
tag: truein bank config
Both merge into a single tags array on each fact.