Security Guide

February 23, 2026 · View on GitHub

Security considerations for Probe CLI and Probe Agent.


TL;DR

  • Input validation prevents injection attacks
  • Path traversal protection built-in
  • File size and regex limits prevent DoS
  • API keys handled via environment variables
  • Bash execution requires explicit opt-in

Input Validation

Query Validation

Probe enforces strict query syntax:

// Valid queries require explicit operators
"authentication AND login"    // ✓ Valid
"authentication OR signup"    // ✓ Valid
"authentication login"        // ✗ Rejected (ambiguous)

Pattern Limits

LimitValueProtection
Max terms1,000Prevents regex explosion
Max patterns5,000Limits complexity
Max regex size8MBMemory protection
Max line length2,000 charsProcessing limits

Regex Escaping

Special characters automatically escaped:

// User input is escaped before pattern matching
query: "function()""function\\(\\)"

Path Security

Path Traversal Prevention

All file paths are canonicalized:

// Paths resolved to absolute form
std::fs::canonicalize(path)

// Prevents:
// - "../../../etc/passwd"
// - Symlink attacks
// - Unicode normalization issues

Allowed Folders

Restrict search scope:

# Environment variable
ALLOWED_FOLDERS=/project1,/project2

# CLI argument
probe search "query" /specific/path

Gitignore Integration

Respects .gitignore rules:

  • node_modules/ excluded
  • .git/ excluded
  • Build directories excluded
  • Custom patterns honored

File Limits

Size Restrictions

LimitValuePurpose
Max file size1MBPrevent memory exhaustion
Max image size20MBLimit upload size
Max line length2,000Skip binary/minified

Timeout Protection

# Default timeout: 30 seconds
probe search "query" ./path --timeout 60

Command Execution

Bash Tool Security

The bash tool requires explicit opt-in:

const agent = new ProbeAgent({
  enableBash: true,
  bashConfig: {
    allow: ['git:push'],           // Custom allow (overrides default deny)
    deny: ['git:push:--force'],    // Custom deny (always wins)
    disableDefaultDeny: false
  }
});

Pattern Syntax

Bash permissions use colon-separated patterns that match commands and arguments:

PatternMatchesDoes NOT match
git:pushgit push, git push origin main, git push --forcegit pull, git status
git:push:--forcegit push --force, git push --force origingit push, git push origin main
git:branch:*git branch -a, git branch --listgit status
npm:installnpm install, npm install expressnpm test

A pattern like git:push matches the command git with first argument push, regardless of any additional arguments. The pattern only checks the parts it specifies — extra arguments in the actual command are ignored.

Note on wildcards: git:push and git:push:* are functionally identical. The * wildcard matches any argument or no argument, so it adds no additional coverage. Prefer the shorter form git:push for clarity.

Permission Resolution Priority

Permissions are resolved using a strict 4-level priority system. Higher priority always wins:

Priority 1 (highest): Custom deny   — --bash-deny patterns, ALWAYS block
Priority 2:           Custom allow  — --bash-allow patterns, override default deny
Priority 3:           Default deny  — built-in deny list, block by default
Priority 4 (lowest):  Allow list    — built-in + custom allow, permit safe commands

How this works in practice:

  1. Custom deny always wins. If a command matches a --bash-deny pattern, it is blocked regardless of any allow patterns. This is the user's explicit "never allow this" list.

  2. Custom allow overrides default deny. If a command matches a --bash-allow pattern, it bypasses the built-in default deny list. This lets users selectively allow specific commands (like git push) without disabling all default protections.

  3. Default deny blocks by default. If a command matches the built-in deny list and is NOT in the custom allow list, it is blocked.

  4. Allow list permits. If a command matches the combined allow list (built-in defaults + custom), it is permitted.

Example: Allow Push but Block Force Push

# CLI usage
probe-chat --enable-bash \
  --bash-allow "git:push" \
  --bash-deny "git:push:--force" "git:push:--force-with-lease" \
  ./my-project
// SDK usage
const agent = new ProbeAgent({
  enableBash: true,
  bashConfig: {
    allow: ['git:push'],
    deny: ['git:push:--force', 'git:push:--force-with-lease']
  }
});
CommandResultReason
git push origin mainAllowedCustom allow git:push overrides default deny
git push --tagsAllowedCustom allow git:push overrides default deny
git push --forceDeniedCustom deny git:push:--force wins over custom allow
git push --force-with-leaseDeniedCustom deny wins over custom allow
git reset --hard HEADDeniedStill in default deny, not overridden
git statusAllowedIn default allow list (no conflict)
rm -rf /DeniedStill in default deny, not overridden

Example: CI/CD Pipeline Permissions

// Allow git operations needed for deployment
const agent = new ProbeAgent({
  enableBash: true,
  bashConfig: {
    allow: ['git:push', 'git:commit', 'git:add', 'npm:install', 'npm:run'],
    deny: [
      'git:push:--force',              // No force push
      'git:push:--force-with-lease',   // No force push variants
      'npm:run:eject',                 // Don't allow eject
    ]
  }
});

Complex Commands (&&, ||, pipes)

When a command contains &&, ||, or |, each component is checked independently using the same priority rules. All components must be allowed for the command to execute:

# Allowed: both components pass
git status && git push origin main   # (with --bash-allow "git:push")

# Denied: git push --force is in custom deny
git status && git push --force       # (with --bash-deny "git:push:--force")

# Denied: rm -rf is in default deny, not overridden
git push && rm -rf /                 # (with --bash-allow "git:push")

Default Deny Patterns

Dangerous commands blocked by default (partial list):

CategoryPatterns
Destructive file opsrm:-rf, rm:-r, rmdir, mkfs
Privilege escalationsudo, su, doas
Git destructivegit:push, git:reset, git:clean, git:commit, git:merge, git:rebase
Git branch/tag deletegit:branch:-d, git:branch:-D, git:tag:-d
GitHub CLI writesgh:issue:create, gh:pr:merge, gh:repo:delete
Package installsnpm:install, pip:install, cargo:install
Network downloadscurl, wget, nc
System operationsshutdown, reboot, mount, crontab

Default Allow Patterns

Safe read-only commands allowed by default (partial list):

CategoryPatterns
File explorationls, cat, head, tail, find, grep, tree
Git read-onlygit:status, git:log, git:diff, git:show, git:branch, git:blame
Git plumbinggit:cat-file, git:ls-files, git:rev-parse, git:merge-base
GitHub CLI readsgh:issue:list, gh:pr:view, gh:repo:list, gh:search:*, gh:api
Package infonpm:list, pip:list, cargo:--version
System infowhoami, pwd, uname, date, env
# Disable ALL default deny patterns (dangerous!)
probe-chat --enable-bash --no-default-bash-deny ./my-project

# Disable ALL default allow patterns (locks out all commands)
probe-chat --enable-bash --no-default-bash-allow ./my-project

These flags are available for edge cases but are not recommended. Use --bash-allow to selectively override specific deny patterns instead.

Safe Subprocess Execution

// Commands use direct execution, not shell
Command::new("go")
  .arg("list")
  .arg("-m")
  .arg("-json")
  .output()

// No shell interpolation possible

API Key Handling

Environment Variables

API keys stored in environment, not code:

# Recommended approach
export ANTHROPIC_API_KEY=sk-ant-...
export OPENAI_API_KEY=sk-...
export GOOGLE_API_KEY=...

No Logging

API keys excluded from logs:

// Keys masked in debug output
console.log('API Key:', maskKey(apiKey));
// Output: "API Key: sk-ant-...xxxx"

Rotation Support

Keys can be rotated without code changes:

# Simply update environment
export ANTHROPIC_API_KEY=new-key-value

Web Interface Security

Authentication

Basic HTTP auth for production:

AUTH_ENABLED=1
AUTH_USERNAME=admin
AUTH_PASSWORD=secure-password

HTTPS Recommendation

Use reverse proxy for HTTPS:

server {
    listen 443 ssl;
    server_name probe.example.com;

    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    location / {
        proxy_pass http://localhost:8080;
    }
}

CORS Headers

Configurable CORS for API access:

Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: Content-Type, Authorization

Edit Tool Security

Explicit Opt-In

Code editing disabled by default:

const agent = new ProbeAgent({
  allowEdit: false  // Default
});

Web Server Control

# Editing disabled by default
probe-chat --web ./project

# Explicit flag required
probe-chat --web --allow-edit ./project

File Validation

  • File must exist before editing
  • Backup created before modification
  • Atomic write operations

Skill Security

Path Validation

Skills must be inside repository root:

// Validated paths
.claude/skills/code-review/SKILL.md  // ✓ Valid

// Rejected paths
../../../etc/SKILL.md                 // ✗ Traversal blocked

Symlinked skill files are skipped:

// Symlinks not followed for security
if (isSymlink(skillPath)) {
  console.warn('Skipping symlinked skill:', skillPath);
  continue;
}

Safe YAML Parsing

YAML parsed with failsafe schema:

// No code execution in YAML
yaml.load(content, { schema: 'failsafe' });

Output Escaping

XML Escaping

All output properly escaped:

fn escape_xml(s: &str) -> String {
    s.replace('&', "&")
     .replace('<', "&lt;")
     .replace('>', "&gt;")
     .replace('"', "&quot;")
     .replace('\'', "&apos;")
}

JSON Serialization

Using standard libraries:

// Safe serialization via serde
serde_json::to_string(&result)

Error Handling

No Sensitive Data in Errors

// Good: Generic error message
Err(anyhow!("Failed to read file"))

// Bad: Exposes system details
Err(anyhow!("Failed to read /etc/passwd: permission denied"))

Context Without Exposure

fs::read_to_string(path)
    .context("Failed to read configuration")?

Resource Limits

Prevent DoS

ResourceLimitProtection
File size1MBMemory
Regex size8MBCPU
Query terms1,000Processing
Timeout30s defaultRunaway operations
Max resultsConfigurableResponse size
Max tokensConfigurableCost control

Token Limits

# Limit response size
probe search "query" ./path --max-tokens 10000

# Limit result count
probe search "query" ./path --max-results 50

Best Practices

1. Production Deployment

# Enable authentication
AUTH_ENABLED=1
AUTH_USERNAME=admin
AUTH_PASSWORD=$(openssl rand -base64 32)

# Use HTTPS (via reverse proxy)
# Restrict allowed folders
ALLOWED_FOLDERS=/app/src

# Disable dangerous features
# Don't use --allow-edit or --enable-bash unless needed

2. API Key Management

# Use environment variables
export ANTHROPIC_API_KEY=...

# Or secret management
source /path/to/secrets.env

# Never commit keys
echo "*.env" >> .gitignore

3. Restrict Bash Execution

const agent = new ProbeAgent({
  enableBash: true,
  bashConfig: {
    allow: [
      'git:push',          // Override default deny for push
      'npm:run:test',      // Allow npm test
    ],
    deny: [
      'git:push:--force',  // But block force push (custom deny always wins)
    ]
  }
});

4. Limit Search Scope

const agent = new ProbeAgent({
  path: './src',
  allowedFolders: ['./src', './lib']
});

5. Monitor Usage

// Track token usage
const usage = agent.getTokenUsage();
if (usage.totalTokens > 1000000) {
  console.warn('High token usage detected');
}

// Enable tracing
probe-chat --trace-file ./audit.jsonl

Security Checklist

Before Production

  • Authentication enabled
  • HTTPS configured (reverse proxy)
  • API keys in environment/secrets
  • Allowed folders restricted
  • Edit tool disabled (unless needed)
  • Bash tool disabled (unless needed)
  • Tracing/logging enabled
  • Timeout limits configured
  • Token limits configured

Ongoing

  • Rotate API keys regularly
  • Review access logs
  • Update dependencies
  • Monitor token usage
  • Audit bash command patterns