Skip to main content

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

FieldTypeDescription
displayNamestringHuman-readable name
membersstring[]Canonical user IDs (file names from users/)
recallbooleanCan read from memory
retainbooleanCan write to memory
retainRolesstring[]Message roles retained: user, assistant, system, tool
retainTagsstring[]Tags added to all retained facts
retainEveryNTurnsnumberRetain every Nth turn
recallBudgetstringRecall effort: low, mid, high
recallMaxTokensnumberMax tokens injected per turn
recallTagGroupsTagGroup[] or nullTag filter for recall. null = no filter.
llmModelstringLLM model for extraction
llmProviderstringLLM provider for extraction
excludeProvidersstring[]Skip these message providers

Merge Rules (multiple groups)

When a user belongs to multiple groups:

FieldRule
recall, retainMost permissive wins (true > false)
retainRoles, retainTagsUnioned
recallBudgetMost permissive (high > mid > low)
recallMaxTokensHighest value wins
recallTagGroupsAND-ed together
llmModel, llmProviderAlphabetically first group that defines it wins
retainEveryNTurnsLowest value wins (most frequent)
excludeProvidersUnioned (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:

  1. Merge global groups — collect all user's groups, merge with rules above
  2. Bank _default baseline — if bank has permissions.groups._default, start there
  3. Bank group overlay — merge bank-level group entries for this user's groups
  4. 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:

  1. Code-levelretainTags from groups + auto user:<id> tag
  2. LLM-extracted — entity labels with tag: true in bank config

Both merge into a single tags array on each fact.