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