Skip to main content
Engram provides defense-in-depth privacy protection by stripping sensitive content at two layers: the plugin layer (before data leaves the process) and the store layer (before any database write).

Privacy Tags

Wrap sensitive content in <private>...</private> tags to redact it from memory:
Set up API with <private>sk-abc123</private> key
Becomes:
Set up API with [REDACTED] key
The sensitive content is permanently removed. Once stripped, it cannot be recovered from Engram’s database.

What Gets Redacted

Any content inside <private> tags is replaced with [REDACTED]:
  • API keys and secrets
  • Passwords and tokens
  • Personal identifiable information (PII)
  • Email addresses, phone numbers
  • Database connection strings
  • Any sensitive configuration values
Added JWT secret <private>super-secret-key-12345</private> to .env file

Configured database: <private>postgres://user:pass@localhost/db</private>

User reported issue via <private>[email protected]</private>

Two-Layer Defense

Engram strips privacy tags at two independent layers to ensure sensitive data never reaches the database:

Layer 1: Plugin Layer (TypeScript)

For agents using Engram plugins (OpenCode, Claude Code), privacy tags are stripped before data is sent to the HTTP API. OpenCode Plugin (engram.ts):
function stripPrivateTags(content: string): string {
  return content.replace(/<private>.*?<\/private>/gis, '[REDACTED]');
}
When it runs:
  • Before sending observations to POST /observations
  • Before sending prompts to POST /prompts
  • Before any HTTP request leaves the plugin
Why? Defense in depth. Even if the store layer fails, sensitive data never leaves the process.

Layer 2: Store Layer (Go)

The SQLite store layer strips privacy tags before any database write, regardless of how the data arrives (HTTP API, MCP stdio, CLI). Implementation (internal/store/store.go):
var privateTagRegex = regexp.MustCompile(`(?is)<private>.*?</private>`)

func stripPrivateTags(s string) string {
    result := privateTagRegex.ReplaceAllString(s, "[REDACTED]")
    result = strings.TrimSpace(result)
    return result
}
When it runs:
  • Inside AddObservation() — strips title and content before INSERT
  • Inside UpdateObservation() — strips title and content before UPDATE
  • Inside AddPrompt() — strips content before INSERT
  • Inside passive capture — strips learning items before save
Why? Guarantees that the database never contains sensitive data, even if called directly via API or CLI without a plugin.

Pattern Details

Regex Pattern

(?is)<private>.*?</private>
Flags:
  • (?i) — Case-insensitive (matches <PRIVATE>, <Private>, etc.)
  • (?s) — Dot matches newlines (supports multiline content)
  • .*? — Non-greedy match (stops at first </private>)

Supported Variations

All of these work:
<private>secret</private>
<PRIVATE>secret</PRIVATE>
<Private>secret</Private>
<private>
  multiline
  secret
</private>

Nested Tags

The regex is non-greedy, so nested tags are handled correctly:
Outer <private>secret1</private> and <private>secret2</private> content
Becomes:
Outer [REDACTED] and [REDACTED] content

Usage Examples

Protecting API Keys

**What**: Added Stripe integration
**Why**: Process payments for premium features
**Where**: src/payments/stripe.ts
**Learned**: Test mode key is <private>sk_test_abc123</private>, live key is <private>sk_live_xyz789</private>
Stored as:
**What**: Added Stripe integration
**Why**: Process payments for premium features
**Where**: src/payments/stripe.ts
**Learned**: Test mode key is [REDACTED], live key is [REDACTED]

Protecting Connection Strings

**What**: Set up production database
**Why**: Deploy to AWS RDS
**Where**: .env.production
**Learned**: Connection string format is <private>postgresql://admin:[email protected]:5432/myapp</private>
Stored as:
**What**: Set up production database
**Why**: Deploy to AWS RDS
**Where**: .env.production
**Learned**: Connection string format is [REDACTED]

Protecting User Data

**What**: Fixed bug in password reset flow
**Why**: User <private>[email protected]</private> reported they couldn't reset their password
**Where**: src/auth/reset-password.ts
**Learned**: Token expiry was set to 1 hour instead of 24 hours
Stored as:
**What**: Fixed bug in password reset flow
**Why**: User [REDACTED] reported they couldn't reset their password
**Where**: src/auth/reset-password.ts
**Learned**: Token expiry was set to 1 hour instead of 24 hours

When to Use Privacy Tags

Always Redact

  • API keys, tokens, secrets
  • Passwords, passphrases
  • Database credentials
  • Personal emails, phone numbers
  • Social Security Numbers, credit cards

Consider Redacting

  • Internal URLs with auth tokens
  • Server hostnames or IPs
  • User-specific identifiers
  • Proprietary business logic
  • Sensitive configuration values
Team collaboration consideration: If you’re using Git Sync to share memories with your team, privacy tags prevent sensitive data from being committed to the repository.Always wrap secrets in <private> tags before saving to memory.

MCP Tool Integration

When calling MCP tools directly (without a plugin), privacy tags are still stripped at the store layer:
{
  "tool": "mem_save",
  "arguments": {
    "title": "Configured OAuth",
    "content": "Client secret: <private>abc123xyz</private>",
    "type": "config"
  }
}
The observation is saved with:
{
  "title": "Configured OAuth",
  "content": "Client secret: [REDACTED]",
  "type": "config"
}

Verification

You can verify that privacy tags are working by:

1. Check the TUI

engram tui
Navigate to the observation and verify that sensitive content shows [REDACTED].

2. Check the Database Directly

sqlite3 ~/.engram/engram.db
SELECT content FROM observations WHERE id = 123;
Verify that the content contains [REDACTED] instead of the sensitive value.

3. Export and Inspect

engram export memories.json
cat memories.json | grep -i private
Should return nothing — no <private> tags should appear in the export.

Best Practices

1

Use privacy tags proactively

Don’t wait until you see a secret in memory. Wrap it in <private> tags before saving.
2

Tag entire values, not just parts

❌ Bad: API_KEY=abc<private>123</private>xyz✅ Good: API_KEY=<private>abc123xyz</private>
3

Use privacy tags in prompts too

When saving user prompts with mem_save_prompt, wrap sensitive content:
User asked: "Can you configure the API with key <private>sk-123</private>?"
4

Review memories before syncing

Before engram sync, review recent memories in the TUI to ensure no secrets leaked through:
engram tui  # Navigate to Recent Observations

Limitations

What privacy tags do NOT protect:
  1. Secrets in file paths: If you save Fixed bug in /home/user/.ssh/id_rsa, the path is stored as-is. Wrap it: Fixed bug in <private>/home/user/.ssh/id_rsa</private>
  2. Secrets in tool output: If an agent runs a command that outputs secrets to its context, those won’t be automatically redacted unless the agent wraps them in privacy tags before saving.
  3. Inference from context: If you write “API key ends in 123”, an attacker with access to your memory could narrow down the key. Be mindful of indirect leakage.

Git Sync

Share memories safely with privacy tags

MCP Tools

Save memories with privacy protection

Export/Import

Export memories with redacted content

Architecture

Learn about Engram’s security layers

Build docs developers (and LLMs) love