Skip to main content

Security

ClawDesk implements a defense-in-depth security architecture with four cascading layers. Every inbound message passes through all four layers before reaching the agent pipeline, and every outbound response is validated before delivery.

Security Architecture

Layer Summary

LayerComponentPurposePerformance
1. AllowlistAllowlistManagerFast identity-based filteringO(1)
2. Content ScanningCascadeScannerMulti-stage content analysisO(n)
3. Access ControlAclManagerRole-based permission checksO(1)
4. AuditAuditLoggerHash-chained audit trailO(1)

Layer 1: Allowlist

The allowlist is the first line of defense—a fast O(1) check that accepts or rejects messages based on sender identity before any further processing.

Configuration

[security.allowlist]
enabled = true
mode = "allowlist" # "allowlist" | "denylist" | "open"

# User allowlist
users = [
"telegram:12345678",
"discord:987654321",
"slack:U01234ABCDE",
"*:admin@company.com", # any channel
]

# Group/channel allowlist
groups = [
"telegram:-100123456789",
"discord:guild:123456789012345678",
"slack:channel:C01234ABCDE",
]

# Wildcard patterns
patterns = [
"telegram:*", # all Telegram users (use with caution)
"slack:U*", # all Slack users
"*:*@company.com", # all company emails across channels
]

Modes

ModeBehavior
allowlistOnly listed identities can interact (default, most secure)
denylistEveryone except listed identities can interact
openNo identity filtering (use with other security layers)

AllowlistManager

pub struct AllowlistManager {
mode: AllowlistMode,
users: HashSet<IdentityPattern>,
groups: HashSet<IdentityPattern>,
patterns: Vec<GlobPattern>,
}

impl AllowlistManager {
/// Check if an identity is allowed
pub fn is_allowed(&self, identity: &Identity) -> bool {
match self.mode {
AllowlistMode::Open => true,
AllowlistMode::Allowlist => {
self.users.contains(&identity.to_pattern())
|| self.groups.contains(&identity.group_pattern())
|| self.patterns.iter().any(|p| p.matches(identity))
}
AllowlistMode::Denylist => {
!self.users.contains(&identity.to_pattern())
&& !self.groups.contains(&identity.group_pattern())
&& !self.patterns.iter().any(|p| p.matches(identity))
}
}
}

/// Add an identity to the allowlist
pub fn add(&mut self, pattern: &str) -> Result<()>;

/// Remove an identity from the allowlist
pub fn remove(&mut self, pattern: &str) -> Result<()>;
}

CLI Management

# View allowlist
clawdesk security allowlist show

# Add entries
clawdesk security allowlist add "telegram:12345678"
clawdesk security allowlist add "discord:987654321"
clawdesk security allowlist add "*:admin@company.com"

# Remove entries
clawdesk security allowlist remove "telegram:12345678"

# Test an identity
clawdesk security allowlist check "telegram:12345678"
# ✅ Allowed: matches rule "telegram:12345678"

Layer 2: Content Scanning

The CascadeScanner analyzes message content through three progressive scanning stages, from fastest to most thorough:

Stage 1: Regex Scanning

Fast pattern matching for known dangerous patterns:

[security.scanning.regex]
enabled = true

# Built-in patterns (always active)
# - SQL injection patterns
# - Shell command injection
# - Path traversal attempts
# - Credential patterns (API keys, tokens)

# Custom patterns
[[security.scanning.regex.patterns]]
name = "company_secrets"
pattern = '(?i)(internal\s+use\s+only|confidential|secret\s+project\s+\w+)'
action = "block" # "block" | "warn" | "redact"
message = "Message contains potentially confidential information"

[[security.scanning.regex.patterns]]
name = "pii_ssn"
pattern = '\b\d{3}-\d{2}-\d{4}\b'
action = "redact"
replacement = "[SSN REDACTED]"

[[security.scanning.regex.patterns]]
name = "profanity"
pattern = '(?i)\b(badword1|badword2)\b'
action = "block"
message = "Message contains prohibited language"

Stage 2: AST Analysis

Structured analysis for code-like content—detects sophisticated injection attempts that regex might miss:

[security.scanning.ast]
enabled = true
languages = ["sql", "javascript", "python", "shell"]
max_parse_size_bytes = 10240

[security.scanning.ast.rules]
# Detect SQL injection via AST parsing
sql_injection = true
# Detect shell command injection
shell_injection = true
# Detect code that accesses the filesystem
filesystem_access = "warn" # "block" | "warn" | "allow"
# Detect network access
network_access = "warn"
pub struct AstScanner {
parsers: HashMap<Language, Box<dyn Parser>>,
rules: AstRules,
}

impl AstScanner {
pub fn scan(&self, content: &str) -> ScanResult {
// Detect the language
if let Some(language) = self.detect_language(content) {
let parser = &self.parsers[&language];
let ast = parser.parse(content);

// Walk the AST looking for dangerous patterns
for node in ast.walk() {
if self.is_dangerous(node, &language) {
return ScanResult::Blocked(ScanReason::AstViolation {
language,
node_type: node.kind().to_string(),
description: self.describe_violation(node),
});
}
}
}

ScanResult::Passed
}
}

Stage 3: Semantic Analysis

AI-powered analysis for subtle threats that bypass pattern matching:

[security.scanning.semantic]
enabled = true
provider = "local" # "local" | "anthropic" | "openai"
model = "classifier-v1" # lightweight classification model
threshold = 0.85 # confidence threshold for blocking
max_latency_ms = 100 # skip if analysis would be too slow

[security.scanning.semantic.categories]
prompt_injection = true
social_engineering = true
data_exfiltration = true
jailbreak_attempt = true
info

Semantic scanning adds latency. Use a lightweight local model for low-latency scanning, or set max_latency_ms to skip semantic analysis when the system is under load.

CascadeScanner

pub struct CascadeScanner {
regex_scanner: RegexScanner,
ast_scanner: AstScanner,
semantic_scanner: Option<SemanticScanner>,
}

impl CascadeScanner {
pub async fn scan(&self, content: &str) -> ScanResult {
// Stage 1: Regex (fast)
let regex_result = self.regex_scanner.scan(content);
if regex_result.is_blocked() {
return regex_result;
}

// Stage 2: AST (medium)
let ast_result = self.ast_scanner.scan(content);
if ast_result.is_blocked() {
return ast_result;
}

// Stage 3: Semantic (slow, optional)
if let Some(semantic) = &self.semantic_scanner {
let semantic_result = semantic.scan(content).await;
if semantic_result.is_blocked() {
return semantic_result;
}
}

ScanResult::Passed
}
}

CLI Testing

# Test content against scanning rules
echo "SELECT * FROM users WHERE id = 1; DROP TABLE users;" | clawdesk security scan

# Output:
# ❌ BLOCKED by regex scanner
# Rule: sql_injection
# Pattern: DROP TABLE
# Action: block

echo "Hello, how are you?" | clawdesk security scan
# ✅ PASSED all scanning stages

Layer 3: Access Control (ACL)

The ACL system provides fine-grained role-based access control.

Roles and Permissions

[security.acl]
enabled = true
default_role = "user"

# Role definitions
[security.acl.roles.admin]
permissions = ["*"] # all permissions

[security.acl.roles.user]
permissions = [
"message:send",
"message:read",
"tools:web_search",
"tools:calculator",
"tools:knowledge_base",
"session:read",
"session:create",
]

[security.acl.roles.restricted]
permissions = [
"message:send",
"message:read",
]

[security.acl.roles.operator]
permissions = [
"message:*",
"session:*",
"tools:*",
"channels:read",
"config:read",
"cron:read",
]

Permission Format

Permissions follow the pattern resource:action:

ResourceActionsDescription
messagesend, read, deleteMessage operations
sessioncreate, read, delete, compactSession management
tools<tool_name>, *Tool access
channelsread, write, adminChannel management
configread, writeConfiguration access
cronread, write, executeCron task management
pluginsread, install, adminPlugin management
securityread, write, adminSecurity settings
**Wildcard (all)

User Assignments

# Assign roles to users
[security.acl.assignments]
"telegram:12345678" = "admin"
"discord:987654321" = "operator"
"slack:U01234ABCDE" = "user"
"*:*@company.com" = "user" # all company emails get user role

AclManager

pub struct AclManager {
roles: HashMap<String, Role>,
assignments: HashMap<IdentityPattern, String>,
default_role: String,
}

impl AclManager {
/// Check if an identity has a specific permission
pub fn has_permission(&self, identity: &Identity, permission: &str) -> bool {
let role_name = self.get_role(identity);
let role = &self.roles[&role_name];
role.has_permission(permission)
}

/// Get all permissions for an identity
pub fn get_permissions(&self, identity: &Identity) -> Vec<String> {
let role_name = self.get_role(identity);
self.roles[&role_name].permissions.clone()
}

/// Grant a role to an identity
pub fn grant(&mut self, identity: &str, role: &str) -> Result<()>;

/// Revoke a role from an identity
pub fn revoke(&mut self, identity: &str) -> Result<()>;
}

CLI Management

# View ACL rules
clawdesk security acl show

# Grant/revoke roles
clawdesk security acl grant "telegram:12345678" admin
clawdesk security acl revoke "telegram:12345678"

# Check permissions
clawdesk security acl check "telegram:12345678" "tools:code_execution"
# ✅ Allowed: role 'admin' has permission 'tools:code_execution'

Layer 4: Audit Logging

The audit system provides a tamper-evident, hash-chained log of all security-relevant events.

Hash Chain

Each audit entry includes a SHA-256 hash of the previous entry, creating an immutable chain:

AuditLogger

pub struct AuditLogger {
store: Box<dyn AuditStore>,
last_hash: Mutex<[u8; 32]>,
config: AuditConfig,
}

pub struct AuditEntry {
pub id: Uuid,
pub timestamp: DateTime<Utc>,
pub event_type: AuditEventType,
pub identity: Option<Identity>,
pub channel_id: Option<ChannelId>,
pub details: serde_json::Value,
pub prev_hash: [u8; 32],
pub hash: [u8; 32],
}

pub enum AuditEventType {
MessageReceived,
MessageSent,
MessageBlocked,
AuthSuccess,
AuthFailure,
ToolExecuted,
ToolBlocked,
ConfigChanged,
PluginLoaded,
PluginUnloaded,
SessionCreated,
SessionDeleted,
AllowlistModified,
AclModified,
ScanningTriggered,
}

impl AuditLogger {
pub async fn log(&self, event: AuditEventType, details: impl Serialize) -> Result<()> {
let mut last_hash = self.last_hash.lock().await;

let entry = AuditEntry {
id: Uuid::new_v4(),
timestamp: Utc::now(),
event_type: event,
details: serde_json::to_value(&details)?,
prev_hash: *last_hash,
hash: [0; 32], // computed below
..Default::default()
};

// Compute hash: SHA-256(prev_hash || timestamp || event || details)
let hash = compute_hash(&entry);
let entry = AuditEntry { hash, ..entry };

// Store
self.store.append(&entry).await?;
*last_hash = hash;

Ok(())
}

/// Verify the integrity of the audit chain
pub async fn verify(&self) -> Result<VerificationResult> {
let entries = self.store.read_all().await?;
let mut expected_prev = [0u8; 32];

for (i, entry) in entries.iter().enumerate() {
// Verify prev_hash links
if entry.prev_hash != expected_prev {
return Ok(VerificationResult::Broken {
entry_index: i,
expected: hex::encode(expected_prev),
found: hex::encode(entry.prev_hash),
});
}

// Verify self hash
let computed = compute_hash(entry);
if entry.hash != computed {
return Ok(VerificationResult::Tampered {
entry_index: i,
entry_id: entry.id,
});
}

expected_prev = entry.hash;
}

Ok(VerificationResult::Valid {
entries_checked: entries.len(),
})
}
}

Configuration

[security.audit]
enabled = true
storage = "file" # "file" | "sqlite" | "postgres"
path = "${CLAWDESK_DATA_DIR}/audit/audit.log"
rotation = "daily" # "hourly" | "daily" | "size:100MB"
retention_days = 90 # how long to keep audit logs
compress_rotated = true

# Events to audit (all enabled by default)
[security.audit.events]
message_received = true
message_sent = true
message_blocked = true
auth_success = false # disable to reduce volume
auth_failure = true
tool_executed = true
tool_blocked = true
config_changed = true
plugin_events = true
session_events = true
security_changes = true

CLI Operations

# Tail audit log
clawdesk security audit tail --follow

# Example output:
# 2026-02-17T14:23:01Z [MessageReceived] telegram:12345678 → tg_main "Hello"
# 2026-02-17T14:23:01Z [AuthSuccess] telegram:12345678 role=user
# 2026-02-17T14:23:02Z [ToolExecuted] web_search by telegram:12345678
# 2026-02-17T14:23:03Z [MessageSent] → telegram:12345678 via tg_main (1.2s)

# Verify chain integrity
clawdesk security audit verify
# ✅ Audit chain valid: 14,302 entries verified, no tampering detected

# Export audit log
clawdesk security audit export --format json --since "2026-02-01" > audit.json
clawdesk security audit export --format csv --last 1000 > audit.csv

# Search audit log
clawdesk security audit search --event MessageBlocked --since "24h"

DM Pairing

The DmPairingManager handles secure pairing for direct message channels, preventing unauthorized users from initiating conversations:

[security.dm_pairing]
enabled = true
mode = "invite" # "open" | "invite" | "approval"
invite_expiry_hours = 24
max_pending = 100

# Auto-approve patterns
auto_approve = [
"*:*@company.com",
]
ModeBehavior
openAny user can start a DM conversation
inviteUsers need an invite code to pair
approvalAdmin must approve each new DM pairing
# Generate an invite code
clawdesk security pairing invite --channel telegram
# Invite code: CLWD-ABC123-XYZ
# Expires: 2026-02-18T14:00:00Z
# Share this code with the user to start a conversation.

# List pending approvals
clawdesk security pairing pending

# Approve/reject
clawdesk security pairing approve telegram:12345678
clawdesk security pairing reject telegram:99999999

Group Policy

The GroupPolicyManager controls agent behavior in group chats:

[security.group_policy]
# How the bot responds in groups
response_mode = "mention" # "all" | "mention" | "reply" | "command"
command_prefix = "/"
mention_name = "@clawdesk"

# Group-specific policies
[security.group_policy.groups."telegram:-100123456789"]
response_mode = "all"
allowed_tools = ["knowledge_base", "calculator"]
max_response_length = 500

[security.group_policy.groups."discord:guild:123456789"]
response_mode = "mention"
allowed_tools = ["web_search"]
rate_limit_per_user = 10 # messages per hour

Scoped Tokens

Create API tokens with limited permissions for integrations:

pub struct ScopedToken {
pub id: TokenId,
pub name: String,
pub scopes: Vec<String>,
pub created_at: DateTime<Utc>,
pub expires_at: Option<DateTime<Utc>>,
pub last_used: Option<DateTime<Utc>>,
pub created_by: Identity,
}
# Create a token
clawdesk security token create \
--name "ci-bot" \
--scope "read:sessions,write:messages" \
--expires "30d"

# Output:
# Token created:
# ID: tok_abc123
# Name: ci-bot
# Scopes: read:sessions, write:messages
# Expires: 2026-03-19T14:00:00Z
# Token: clwd_sk_abcdef123456...
#
# ⚠️ Save this token — it won't be shown again.

# List tokens
clawdesk security token list

# Revoke a token
clawdesk security token revoke tok_abc123

Using Tokens

# API authentication with a scoped token
curl -H "Authorization: Bearer clwd_sk_abcdef123456..." \
http://localhost:1420/api/v1/sessions

Server Secret

The ServerSecret is used for encrypting stored credentials and signing tokens:

[security]
# Auto-generated on first run, stored encrypted
# Override with environment variable for deployments
server_secret = "${CLAWDESK_SERVER_SECRET}"
# Rotate the server secret (re-encrypts all stored credentials)
clawdesk security secret rotate

# ⚠️ This will:
# 1. Generate a new server secret
# 2. Re-encrypt all stored channel tokens
# 3. Invalidate all active API tokens
# 4. Require re-authentication for all providers
# Continue? (y/N)
warning

Rotating the server secret invalidates all existing scoped tokens and re-encrypts stored credentials. Plan this operation during a maintenance window.


Security Best Practices

  1. Always use allowlist mode in production — mode = "allowlist" is the default and most secure option.

  2. Enable all scanning stages — the cascade scanner is fast thanks to the regex-first approach. Only skip semantic scanning if latency is critical.

  3. Use least-privilege roles — assign the most restrictive role that still allows the user to function.

  4. Verify audit integrity regularly — run clawdesk security audit verify on a schedule.

  5. Rotate secrets periodically — rotate the server secret and API tokens at least quarterly.

  6. Use environment variables for secrets — never store API keys, tokens, or secrets directly in config files.

  7. Enable DM pairing — in production, use invite or approval mode to prevent unauthorized DM interactions.

  8. Monitor audit logs — set up alerts for MessageBlocked, AuthFailure, and ToolBlocked events.


Troubleshooting

ProblemSolution
"User not allowed"Check allowlist with clawdesk security allowlist check <identity>
"Permission denied"Verify role assignment with clawdesk security acl check <identity> <permission>
"Message blocked by scanner"Test content with clawdesk security scan, check regex patterns
"Audit verification failed"Check for filesystem corruption, restore from backup
"Token expired"Create a new token with clawdesk security token create
Cannot decrypt credentialsVerify CLAWDESK_SERVER_SECRET is set correctly