Architecture & Internals
sekretbarilo is a high-performance secret scanner written in Rust. This page explains how it works internally, from the ground up.
Project Structure
src/
main.rs - cli entry point, hand-rolled command parsing
lib.rs - library exports
agent/
mod.rs - check-file command, path resolution, stdin json parsing
claude.rs - claude code hook installation (settings.json)
codex.rs - check-codex command, codex cli hook installation
apply_patch.rs - extraction of added lines from codex apply_patch payloads
hooks_json.rs - shared reader/writer for claude and codex hook json
audit/
mod.rs - working tree scanning
history.rs - git history scanning with branch resolution
search.rs - user-search pass (--search / --search-regex)
config/
mod.rs - config loading and merging
allowlist.rs - allowlist compilation
discovery.rs - hierarchical config file discovery
merge.rs - config merge logic
rules.toml - 112 built-in detection rules
diff/
mod.rs - git diff retrieval
parser.rs - unified diff parser
doctor/mod.rs - diagnostic health checks
hook/mod.rs - git pre-commit hook installation
output/
mod.rs - finding reports
masking.rs - secret value masking
scanner/
engine.rs - main scanning pipeline
rules.rs - rule compilation
entropy.rs - shannon entropy calculation
hash_detect.rs - hash detection (sha-1, sha-256, md5)
password.rs - password strength heuristics
pubkey.rs - public key block detection and tracking
Scanning Pipeline (Core Engine)
The scanner processes files through a multi-stage pipeline optimized for speed and accuracy:
1. Git Diff Retrieval
git diff --cached --unified=0 --diff-filter=d
- retrieves only staged changes
--unified=0minimizes context (we only scan added lines)--diff-filter=dexcludes deleted files (no scanning needed)
2. Unified Diff Parsing
- splits diff into
DiffFilestructs with metadata:path: file pathis_new,is_deleted,is_renamed,is_binary: file status flagsadded_lines: vector ofAddedLinestructs withline_numberandcontent
- handles multi-file diffs, multiple hunks per file, and edge cases:
- binary files (via
Binary files ... differmarker) - renames (extracts final path from
+++ b/header) - root commits (with
--rootflag)
- binary files (via
3. .env File Blocking
- immediate, unconditional block for
.env,.env.local,.env.production - excludes
.env.example,.env.sample,.env.template(safe examples) - prevents accidental exposure before scanning
- reported as rule
env-file-blockedwithline: -andmatch: (blocked file type)— the file is never read
4. Global Path Allowlist Check
pre-filters files to skip scanning:
- binary extensions:
.png,.jpg,.exe,.so,.woff2, etc. - vendor directories:
node_modules/,vendor/,.venv/, etc. - generated files:
package-lock.json,Cargo.lock, minified.min.js - documentation:
README.md,docs/,*.rst(with entropy bonus)
5. Public Key Block Suppression
- tracks multi-line PEM/PGP public key blocks via
PubKeyBlockTracker - when
detect_public_keysis disabled (default), lines inside public key blocks are skipped entirely - prevents base64 content in public keys from triggering token rules (e.g.,
EAA→facebook-access-token) - also detects single-line OpenSSH public keys (
ssh-rsa AAAA..., etc.) - gated rules (
pem-public-key,pgp-public-key-block,openssh-public-key) are skipped unless enabled
6. Aho-Corasick Keyword Pre-filter
- single-pass scan across all rules’ keywords simultaneously
- case-insensitive matching via aho-corasick automaton
- builds a bitset of “candidate rules” whose keywords matched
- drastically reduces regex evaluations: a line that matches no keyword never reaches a regex
Example: Line contains “akia” → activates aws-access-key-id rule
7. Regex Evaluation
- only evaluates regexes for rules whose keywords matched
- uses
regex::bytescrate for binary-safe matching - extracts full matches or capture groups via
secret_groupfield - handles multiple matches per line (iterates
captures_iter)
Example: (AKIA[A-Z0-9]{16}) matches AKIAIOSFODNN7EXAMPLE
8. Secret Extraction
- if
secret_group > 0, extracts capture group value - if
secret_group == 0, uses full match - skips empty matches
Exemption layer (rule generic-high-entropy-value only, [settings] exemption_layer, default on). The extracted value passes a sequence of structural predicates over its bytes; the first one that matches suppresses the finding. The assignment key is never consulted:
- file — ignore files (
.gitignore,.dockerignore,.npmignore,.prettierignore,.eslintignore,.gitattributes,.helmignore) andCODEOWNERSdisable this rule alone; every other rule still runs on them - import — the surrounding line is an import or include statement
- markdown — a markdown link is retargeted to its target and evaluation continues on the inner value; only a target shorter than 20 bytes ends here
- path — the value is path-shaped
- pin — a digest pinned behind a reference, as a workflow
uses:line writes it - url — a URL with no credential in any component. A URL-shaped value can leave the layer only through this step, so steps 7 and 8 are skipped for one
- syntax — for unquoted and bare captures, a complete source expression covering the flagged range (
src/scanner/syntax.rs). An inner token of 20 bytes or more vetoes the span when its entropy reaches 4.0 or its length is exactly 32, 40 or 64 hex digits, so an expression can never cover a credential - wordshape — words, camel case or snake case rather than an opaque run
With the layer enabled, a bounded lexical pass also collects quoted call-argument bodies once per logical line, independently of the regex capture cursor. Ordinary single/double quotes and Rust raw/byte delimiters feed exact body ranges into the shared generic entropy evaluator, with no key, import or syntax exemption, or markdown retargeting. Redaction preserves surrounding delimiters and other arguments. Body-level path, pin, URL and wordshape exemptions retain their recall costs; dense regex literals may become findings. The collector does not join multiline call fragments or support Python-specific literal forms or Go backticks; a complete literal in an unfinished same-line call is still collected. Keyless call bodies receive no assignment hex bypass, so hex bodies below the ordinary entropy threshold remain undetected. Disabling the layer disables collection as well as exemptions.
The layer changes which values are considered, never the gates they are measured against: the 20-byte minimum and the 4.0 threshold are unchanged, and tier 1 and tier 2 rules never enter it. The path-shape check that runs before the layer, and the user entropy-key allowlist that runs after it, are both independent of the switch.
Hex bypass: an exact-length hex value (32, 40 or 64 digits, optionally 0x-prefixed) assigned under a key that is not *_id, *_hash or address-shaped skips the Shannon gate of step 14 and is emitted if it clears every other gate. This is the one place the layer makes the rule stricter. It applies to double-quoted, single-quoted, bracketed and unquoted captures, never to bare lines or call bodies, and requires both that the line carry no hash context (step 12) and that the value reach 2.0 bits of entropy over the 16-symbol hex alphabet.
Rationale and residuals: ADR 0002.
9. Per-Rule Allowlist Check
each rule can define:
- value regexes: patterns to match against extracted secret (e.g.,
AKIAIOSFODNN7EXAMPLE) - path patterns: file path regexes to skip (e.g.,
test/.*) - key patterns: assignment-key wildcard patterns to skip (e.g.,
TMPDIR,GHOSTTY_*); applies only togeneric-high-entropy-valuematches
10. Variable Reference Detection
skips values that are template variables, not real secrets:
$VAR,${VAR},%VAR%(shell variables)process.env.VAR(node.js)os.environ['VAR'](python)ENV['VAR'](ruby)
11. Stopword Filtering
rules with entropy_threshold (tier 2+) check for common safe words:
- built-in:
test,example,fake,placeholder,changeme,dummy,mock - user-configurable via
[allowlist] stopwords = [...] - tier 1 rules (no entropy threshold) only check placeholder patterns (
XXXX...,****...) to allow tokens likesk_test_that inherently contain “test” password-in-urlis neither: it checks the value against a fixed placeholder list (is_url_password_placeholderinsrc/scanner/password.rs— exact matches likepassword,changeme,example,test,<PASSWORD>-shaped brackets,xxx.../***...runs, andyour/myplus a password word) plus the user’s own[allowlist] stopwords, not the built-in stopword list above
12. Hash Detection
prevents false positives on git commit hashes and checksums:
- full-length hashes: 32 (md5), 40 (sha-1), 64 (sha-256) hex chars
- abbreviated hashes: 7-12 hex chars
- requires context keywords on the same line:
commit,sha,hash,checksum,digest,integrity - uses word-boundary matching to avoid false matches (
hashinsideHashMap)
Example: sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 → skipped
This line-level hash context keeps precedence over the hex bypass of step 8: a hex value on a line carrying a hash context word is a hash, not a secret.
13. Password Strength Heuristics
for generic-password-assignment only:
- weak passwords allowed:
password,admin,123456,changeme - strong passwords blocked: complex passwords with high entropy + character classes
- scoring:
- shannon entropy (0-5)
- character class bonus (uppercase + lowercase + digits + special ≥ 3 → +1.0, all 4 → +2.0)
- length bonus (≥12 chars → +0.5, ≥20 chars → +1.0)
- dictionary penalty (-4.0)
- short-value penalty (< 6 chars → -2.0)
- score is clamped at 0.0; threshold: 6.0
Rationale: password=test is a placeholder, password=Kj8#mP2!xQ9vL4nR is a real secret
password-in-url does not use this heuristic. It captures any length of password (no minimum) and
is filtered at step 11 instead, by is_url_password_placeholder (src/scanner/password.rs) plus
the user’s own stopwords, so a weak but non-placeholder URL password is still reported.
14. Shannon Entropy Evaluation
for rules with entropy_threshold set:
- calculates shannon entropy over all 256 byte values
- min length: 20 characters (shorter strings skip entropy check)
- documentation file bonus: +1.0 to threshold (raises bar for false positives in docs)
- global override:
--entropy-thresholdor[settings] entropy_threshold = 3.5sets a floor
Formula: H = -Σ(p_i * log2(p_i)) where p_i is frequency of byte i
Example: aaaaaaaaaaaaaaaaaaaaaaaa → entropy ≈ 0.0 (blocked), aB3dEf7hIj1kLmN0pQrStUvWxYz → entropy ≈ 4.2 (allowed)
Hex bypass: a value admitted by the hex bypass of step 8 skips this gate and nothing else; the allowlists, variable-reference detection and stopwords still apply to it.
Baseline: the measured entropy of short random passwords, and why the 20-character and 4.0 gates are left unchanged, are recorded in ADR 0001.
15. Output with Masking
- diagnostic secret values masked:
AK**************FG(first 2 + last 2 chars) - short values (≤5 chars): fully masked with one
xper character (abc→xxx) - prefixes:
[ERROR](scan),[AUDIT](audit),[AGENT](check-file / check-codex),[SEARCH](user-search pass)
redact-claude uses full [REDACTED] replacement instead of diagnostic prefix/suffix masking.
Audit Pipeline
Working Tree Mode (sekretbarilo audit)
- enumerate tracked files:
git ls-files -z(nul-delimited for safe filenames) - optional: include ignored files:
git ls-files -z --others --ignored --exclude-standard - apply exclude/include patterns: filter via regex (from
[audit]section) - read files in parallel: rayon thread pool, converts to synthetic
DiffFilestructs - feed through scanner engine: same pipeline as scan mode
- report findings: grouped by file, with masked values
Optimizations:
- parallel file reads via rayon (4+ files)
- binary detection: checks first 8KB for null bytes
- error handling: read failures reported but don’t stop scanning
Git History Mode (sekretbarilo audit --history)
- list commits:
git rev-list --all --format=%H%n%an%n%ae%n%aI%n%at- optional filters:
--branch,--since,--until - parses: hash, author, email, date, timestamp (unix)
- optional filters:
- identify root commits:
git rev-list --max-parents=0 --all- required for correct scanning (root commits need
--rootflag)
- required for correct scanning (root commits need
- extract per-commit diffs:
git diff-tree -p --no-commit-id -r --unified=0 --diff-filter=d -m <hash>-msplits merge commits into individual diffs per parent (catches secrets in conflict resolution)--rootfor root commits
- parallel commit processing: rayon parallelizes commit scanning
- progress reporting every 50 commits
- error count tracked atomically
- deduplication: same secret + same file + same rule = keep earliest commit by timestamp
- key:
(file, rule_id, matched_value)→ earliestHistoryFinding - sorts by timestamp, then commit hash, then file, then line
- key:
- branch resolution:
git branch --contains <hash> --format=%(refname:short)- only queries commits with findings (not all commits)
- parallel resolution via rayon
- best-effort: failures warned but don’t stop reporting
- report findings: grouped by commit, shows author email + branches
- sanitizes output: strips control chars and bidi overrides (prevents terminal injection)
- commit hashes are abbreviated in the report
User-Search Pass (--search / --search-regex)
an additive pass that runs alongside the rule-based audit, never instead of it:
- compile patterns:
--searchliterals and--search-regexpatterns (both repeatable) - reuse the same file set: whatever the audit pass enumerated, after exclude/include filtering
- report separately: matches are printed under
[SEARCH]with a trailing count line - unmasked: search hits show the matched text verbatim — the user asked for this exact string
audit-only by design: the flags are rejected on scan. a search match alone is enough to make the
command exit 1, even when the rule-based pass found nothing.
Check-File Pipeline (Claude Code Agent Hook)
Triggered by claude code when reading a file via the Read tool.
Input Modes
- stdin json (
--stdin-json): reads hook payload from stdin{ "tool_input": {"file_path": "/path/to/file.rs"}, "cwd": "/project/root" } - direct path:
sekretbarilo check-file src/config.rs
Pipeline
- parse stdin json payload (if
--stdin-json):- reads up to 1 MB from stdin (prevents unbounded memory)
- extracts
file_pathand optionalcwd
- resolve file path:
- absolute paths: computes relative path from
cwdfor better vendor/pattern detection - relative paths: validates against path traversal (blocks
../../etc/passwd) - returns
(relative_path, base_dir)
- absolute paths: computes relative path from
-
validate base directory: checks
base_dir.is_dir() -
.env blocking: unconditional block for
.envfiles (same policy as scan) - fast-path rejection (default allowlist only):
- uses hardcoded patterns (binary, vendor, lock files)
- skips config loading for obvious skips (performance optimization)
- load hierarchical config (trusted loader):
- discovers configs from
base_dirup to home directory - merges all found configs
- a
.sekretbarilo.tomlinside the git working tree is honoured only when it is git-tracked and unmodified againstHEAD; otherwise the whole layer is dropped with[WARN] ignoring untrusted in-workspace config: <path> - this trust check applies to the agent-hook paths only (
check-file,check-codex,redact-claude) —scanandauditload in-workspace configs unconditionally. see Configuration for the full rules
- discovers configs from
- full fast-path rejection (with user config):
- applies user-defined allowlist paths
- applies audit exclude patterns
- read file:
- converts to synthetic
DiffFile(all lines treated as “added”) - binary detection: first 8KB null byte check
- error handling: read errors block (fail closed)
- converts to synthetic
-
compile scanner: builds
CompiledScannerfrom merged rules -
scan: runs through scanner engine (same pipeline as scan mode)
- report findings (to stderr):
[AGENT] secret(s) detected in /path/to/file.rs file: file.rs line: 42 rule: aws-access-key-id match: AK**************FG file contains 1 secret(s). reading blocked to prevent secret exposure. - exit code:
0= clean (claude reads file)2= secrets found or error (claude blocks read)
Key Design: fail closed. errors (read failure, config error) exit 2 to prevent exposing secrets.
Redact-Claude Pipeline (Claude Code PostToolUse)
sekretbarilo redact-claude --stdin-json receives the successful tool result, after execution. It is an in-memory output editor: it does not read or modify the source file to redact it.
- bounded input: read at most 10 MiB and parse the PostToolUse payload.
- select text fields: Bash stdout/stderr and text blocks, text-file Read content, and Grep content/result lines and filename arrays. Preserve the complete response structure, counters, and unknown metadata.
- trusted configuration: load the existing trusted layers; use rules, entropy, password heuristics, and value exceptions without path exclusions or documentation relaxations.
- exact ranges: the shared scanner exposes captured byte ranges through
scan_text, while the existingscan/Findinginterface remains intact. Extend key-block ranges, merge overlaps, and apply the pureredact_textfunction, preserving UTF-8 and CR/LF. - bounded replacement: return exit 0 with
hookSpecificOutput.updatedToolOutput, or no stdout when clean. Limit the complete serialized response to 10 MiB. - errors: return
continue: falseand a fixed safe reason, plus a replacement hiding every supported text field when its structure is available and the fallback fits. Use fallible stdout/stderr writes; failed stdout delivery exits 1 with a safe stderr diagnostic if possible. A PostToolUse exit 2 cannot remove existing output.
For supported formats, installation/version requirements, and failure/telemetry limitations, see Agent Hooks.
Check-Codex Pipeline (Codex CLI Agent Hook)
Triggered by the Codex CLI before it runs a tool, not after. check-codex inspects what the agent
is about to write, so a secret is caught before it reaches the working tree.
The hook is registered with matcher ^(apply_patch|Bash)$ and runs
sekretbarilo check-codex --stdin-json. The command is internal: it only reads a hook payload from
stdin, and refuses to run without --stdin-json.
Payload
{
"hook_event_name": "PreToolUse",
"tool_name": "apply_patch",
"tool_input": {"command": "*** Begin Patch\n..."},
"cwd": "/project/root"
}
a payload that does not match this schema is rejected with
Codex hook payload schema mismatch: <detail> and exit 2 — fail closed, same as check-file.
Pipeline
- parse payload: 10 MiB stdin limit
- extract scannable text, by tool:
apply_patch: parses the patch envelope and takes only the added lines, keeping the target path so findings are attributed to the file the agent is creating or editingBash: scans the command string itself, attributed to the synthetic path<bash-command>
- load config through the same trusted loader as
check-file(see step 6 above) - scan through the shared scanner engine
- report findings (to stderr):
[AGENT] Codex apply_patch blocked: secret(s) detected file: src/creds.py line: 1 rule: github-personal-access-token match: gh**************************************AB apply_patch action blocked to prevent secret exposure. total findings: 1. - exit code:
0= clean (codex runs the tool)2= secrets found or error (codex blocks the tool call)
Codex-specific caveat: Codex silently skips hooks it has not been asked to trust. Installing the
hook is not enough — it must also be approved with /hooks in the Codex TUI, and doctor reports
the approval entry separately from the installation itself.
Configuration System
Hierarchical Discovery
searches for .sekretbarilo.toml in order (lowest → highest priority):
/etc/sekretbarilo/sekretbarilo.toml(system-wide)~/.config/sekretbarilo/sekretbarilo.toml(user).sekretbarilo.tomlin each directory from repo root → home
merge strategy:
- scalars: highest priority wins (e.g.,
entropy_threshold) - lists: concatenated + deduplicated (e.g.,
stopwords,paths) - rules: same
idoverrides, newidappends
trust: on the agent-hook paths (check-file, check-codex, redact-claude) an in-workspace config layer is
dropped unless it is git-tracked and clean against HEAD — an agent that writes its own
.sekretbarilo.toml cannot allowlist its way past the scanner. scan and audit are unaffected.
Configuration documents the exact conditions.
TOML Format
[settings]
entropy_threshold = 3.5
[allowlist]
paths = ["test/.*", "vendor/.*"]
stopwords = ["my-project-safe-token"]
[[allowlist.rules]]
id = "aws-access-key-id"
regexes = ["AKIAIOSFODNN7EXAMPLE"]
paths = ["test/.*"]
# key patterns apply only to generic-high-entropy-value assignment matches
[[allowlist.rules]]
id = "generic-high-entropy-value"
keys = ["TMPDIR", "GHOSTTY_*"]
[audit]
include_ignored = true
exclude_patterns = ["^build/", "^dist/"]
include_patterns = ["\\.rs$"]
[[rules]]
id = "custom-token"
description = "Custom API token"
regex = "(CUSTOM_[A-Z]{10})"
secret_group = 1
keywords = ["custom_"]
entropy_threshold = 3.5
Rule Compilation
- load default rules: embedded
config/rules.toml(112 rules) - merge user rules: overrides by
id, appends new rules - compile regexes:
regex::bytes::RegexBuilderwith 1 MB size limit - build aho-corasick automaton: all keywords (case-insensitive, deduplicated)
- map keywords → rules:
keyword_to_rules[pattern_idx] = [rule_idx, ...]
Result: CompiledScanner struct with automaton, keyword_to_rules, rules
Hook Installation
Pre-Commit Hook
local: .git/hooks/pre-commit
sekretbarilo install pre-commit
global: ~/.config/git/hooks/pre-commit + git config --global core.hooksPath
sekretbarilo install pre-commit --global
generated script:
#!/bin/sh
# sekretbarilo pre-commit hook
# resolve the sekretbarilo binary
SEKRETBARILO_BIN=""
if command -v sekretbarilo >/dev/null 2>&1; then
SEKRETBARILO_BIN="sekretbarilo"
elif [ -x "$HOME/.cargo/bin/sekretbarilo" ]; then
SEKRETBARILO_BIN="$HOME/.cargo/bin/sekretbarilo"
fi
if [ -n "$SEKRETBARILO_BIN" ]; then
"$SEKRETBARILO_BIN" scan
exit_code=$?
if [ $exit_code -eq 1 ]; then
exit 1
elif [ $exit_code -ne 0 ]; then
echo "[ERROR] sekretbarilo exited with code $exit_code" >&2
exit $exit_code
fi
else
echo "[WARN] sekretbarilo not found in PATH or ~/.cargo/bin/, skipping secret scan" >&2
echo "[WARN] install with: cargo install sekretbarilo" >&2
fi
# end sekretbarilo
features:
- POSIX-compatible (no bashisms)
- idempotent: detects existing installation via marker comment
- appends to existing hooks (inserts before trailing
exit) - graceful degradation: warns if binary not found but doesn’t fail
- sets executable permission (
chmod +x)
Agent Hook (Claude Code)
local: .claude/settings.json
sekretbarilo install agent-hook claude
global: ~/.claude/settings.json
sekretbarilo install agent-hook claude --global
--mode block|redact selects the Claude mode. Omitting it preserves the selected file’s installed mode, defaulting to block for a new installation. Redaction requires Claude Code >= 2.1.121 before any change to Claude settings; it installs synchronous PostToolUse / ^(Bash|Read|Grep)$ with a 10-second timeout.
The installer records the absolute path reported by the OS for the running executable and quotes shell-sensitive paths; it warns and uses the bare sekretbarilo name only when the path lookup fails or is not valid UTF-8. On macOS, an invocation through a symlink such as Homebrew’s /usr/local/bin/sekretbarilo keeps that path. On Linux, current_exe reports the resolved target, so Homebrew-on-Linux records a Cellar path; when brew upgrade removes that target, the hook remains broken until installation is rerun from the new binary, and doctor reports the old path as missing. Doctor checks an absolute configured path’s filesystem metadata, executable bit, and canonical identity relative to the running binary. It never executes a binary selected by Claude settings and does not report a version for it; a bare name receives a warning because Claude Code resolves it under its own PATH.
settings.json structure (block):
{
"hooks": {
"PreToolUse": [
{
"hooks": [
{
"command": "<absolute-path-to-running-sekretbarilo> check-file --stdin-json",
"statusMessage": "Scanning file for secrets...",
"timeout": 10,
"type": "command"
}
],
"matcher": "Read"
}
]
}
}
features:
- reads/creates
.claude/settings.jsonor~/.claude/settings.json - idempotent: detects entries across events, updates the selected or preserved mode, removes duplicate sekretbarilo handlers
- preserves existing settings and other hook matchers
- preserves other handler/group order; reports local/global blocking-Read conflicts with redaction
- atomic write: temp file + rename (prevents corruption)
Agent Hook (Codex CLI)
local: .codex/hooks.json
sekretbarilo install agent-hook codex
global: $CODEX_HOME/hooks.json, defaulting to ~/.codex/hooks.json
sekretbarilo install agent-hook codex --global
hooks.json structure:
{
"hooks": {
"PreToolUse": [
{
"hooks": [
{
"command": "sekretbarilo check-codex --stdin-json",
"statusMessage": "Scanning tool input for secrets...",
"timeout": 10,
"type": "command"
}
],
"matcher": "^(apply_patch|Bash)$"
}
]
}
}
features:
- shares its json reader/writer with the claude installer (
agent/hooks_json.rs), so idempotency, preservation of unrelated entries and atomic writes behave identically - reports the detected Codex version when
codexis onPATH - warns, on every install, that Codex ignores unapproved hooks until you run
/hooksin its TUI
Install All
sekretbarilo install all # pre-commit + claude + codex, locally
sekretbarilo install all --global # the same three, globally
runs the three installers in sequence and prints each one’s result. accepts --mode block|redact for Claude with the same preservation/default/version policy as the standalone installer.
Doctor
sekretbarilo doctor runs five groups of checks and prints each result with a status label:
| Group | Checks |
|---|---|
git pre-commit hook |
local and global hook present, carries the sekretbarilo marker |
claude code agent hook |
local and global settings.json entries, command metadata, and canonical identity relative to the running binary; the configured binary is never executed |
codex cli agent hook |
local and global hooks.json entries, the matching approval entry in Codex’s config.toml, and whether codex is on PATH |
configuration |
which config files were discovered, whether an in-workspace layer is untrusted, rule count, rule compilation |
sekretbarilo binary |
binary reachable on PATH |
status labels are [OK], [WARN], [ERROR] and [NOT INSTALLED]. only [WARN] and [ERROR]
count as issues: doctor exits 1 if any check is an issue, 0 otherwise. a missing hook is
[NOT INSTALLED], which is informational and does not by itself fail the command.
the codex approval check is deliberately weaker than the others. it looks for a positional entry
under [hooks.state] in Codex’s config.toml and does not validate Codex’s own trust hash, so an
[OK] there means “an approval entry exists”, not “the hook will definitely run”. it can never
produce a false [OK] for a missing approval, only an over-optimistic one for a stale hash.
Output & Masking
Masking Strategy
diagnostic secret values masked before display:
| Length | Display |
|---|---|
| 1-5 chars | xxx — one x per character, length preserved, value fully hidden |
| 6+ chars | AB**********YZ (first 2 + last 2) |
rationale: prevents accidental exposure in logs/screenshots while allowing identification
Output Prefixes
[ERROR]: scan command findings (blocks commit), and fatal errors on any command[AUDIT]: audit command findings and progress (informational)[AGENT]: check-file findings (blocks read) and check-codex findings (blocks the tool call)[SEARCH]: user-search pass results — not masked[OK]/[WARN]/[NOT INSTALLED]/[INFO]: install and doctor status lines
diagnostics go to stderr. redact-claude writes replacement/stop JSON to stdout, using full [REDACTED] values and preserving the response structure.
Terminal Safety
history mode sanitizes all displayed fields:
- strips control characters (
\x00-\x1f,\x7f) - strips bidi overrides (
\u{202A}-\u{202E}) - prevents terminal injection via malicious git author/email/branch/file names
Design Decisions
Fail Closed
errors exit 2 (block) rather than 0 (allow):
- config parse errors → block
- file read errors → block
- rule compilation errors → block
rationale: better to over-block than expose secrets
No Network
everything runs locally:
- no telemetry
- no remote rule updates
- no API calls
rationale: privacy, security, offline support
POSIX Hooks
generated hooks use only POSIX shell features:
[ ]not[[ ]]command -vnotwhich"$VAR"quoting everywhere
rationale: works on all shells (sh, bash, zsh, dash)
Graceful Degradation
hooks warn but don’t fail if binary not found:
[WARN] sekretbarilo not found in PATH or ~/.cargo/bin/, skipping secret scan
rationale: doesn’t break workflows if binary is temporarily missing
Trusted Config on Agent Paths
check-file, check-codex, and redact-claude drop an in-workspace .sekretbarilo.toml unless git says it is
tracked and unmodified:
[WARN] ignoring untrusted in-workspace config: /project/root/.sekretbarilo.toml
rationale: the agent being scanned can write files in the workspace. if an untracked config
counted, an agent could allowlist itself past the hook it is supposed to be constrained by.
scan and audit are run by a human and keep the unconditional behaviour.
Stdin Limit
check-file limits stdin to 1 MB:
std::io::stdin().take(1_048_576).read_to_string(&mut input)
check-codex and redact-claude cap input at 10 MiB. Redaction also caps its serialized hook response at 10 MiB.
rationale: prevents unbounded memory consumption from malicious payloads
Regex Size Limit
all regexes compiled with 1 MB limit:
RegexBuilder::new(&pattern).size_limit(1 << 20).build()
rationale: prevents ReDoS (regular expression denial of service)
Parallel Processing
uses rayon for parallel file/commit processing:
- threshold: 4+ files/commits
- automatic work-stealing
- respects available CPU cores
rationale: 10-100x speedup on large repos
Binary-Safe Scanning
uses regex::bytes and Vec<u8> everywhere:
- handles non-UTF-8 files
- no allocation for UTF-8 conversion
rationale: supports all file encodings
Performance Characteristics
Scan Mode (Staged Changes)
per-invocation wall clock, measured on macOS 15.7 / Intel Core i9-9900K @ 3.60GHz, 50 invocations
of sekretbarilo scan against a one-file staged diff:
| Component | Cost |
|---|---|
process spawn (--version as a floor) |
~13 ms |
git diff --cached subprocess |
~10 ms |
rule compilation (112 rules, delta vs --no-defaults) |
~11 ms |
whole sekretbarilo scan invocation |
~53 ms |
the in-process scan itself is microseconds (Performance); essentially all of the wall clock is fixed startup cost, so it does not grow with commit size until the diff is very large. these figures are hardware- and OS-dependent — treat them as a shape, not a guarantee.
bottleneck: fixed startup, not scanning. regex evaluation is the scanning-side cost, mitigated by the aho-corasick pre-filter.
Audit and History Modes
no published figures. both are dominated by I/O and by git subprocesses rather than by the
scanner, so they track repository size, filesystem speed and core count more than anything
sekretbarilo controls. measure on your own repository:
time sekretbarilo audit
time sekretbarilo audit --history
audit bottleneck: file I/O, mitigated by parallel reads.
history bottleneck: git diff-tree execution, mitigated by parallel commit processing and by
resolving branches only for commits that produced findings.
Optimization Techniques
- aho-corasick pre-filter: most lines match no keyword at all and never reach a regex
- rayon parallelism: audit and history work scales with available cores
- binary-safe bytes: no UTF-8 conversion overhead
- reusable bitsets: avoids per-line allocations
- early termination: skips binary/vendor/lock files immediately
Testing Strategy
Unit Tests
- scanner engine: keyword matching, regex extraction, entropy calculation
- diff parser: edge cases (binary, renames, root commits, multiple hunks)
- config merging: scalar override, list concatenation, rule merging
- allowlist compilation: path patterns, stopwords, per-rule allowlists
Integration Tests
one file per area under tests/:
- default rules: all 112 rules compile and detect known secrets
- false positives: a corpus that must stay clean
- hook installation: idempotency, appending, preservation of existing hooks
- claude agent hook: JSON parsing, path resolution, fast-path rejection
- codex agent hook: payload schema,
apply_patchandBashextraction, config trust - doctor: each check group, and the exit code it produces
- audit, history and the user-search pass
- public key suppression, config merge precedence, CLI flag overrides
- hook panic safety: a panic must not turn into a silent allow
git-dependent tests build temp repos with tempfile and are serialized with serial_test, since
they mutate global git state.
Security Considerations
Threat Model
in-scope:
- accidental secret commits (developer error)
- copy-paste from documentation (example secrets)
-
weak passwords in config files
- an AI agent writing a secret into the working tree, before the write lands (
check-codex) - an AI agent reading a file that already contains one (
check-file) - detected secrets in successful Claude Bash/Read/Grep text results (
redact-claude)
out-of-scope:
- intentional malicious commits (insider threat)
- encrypted/obfuscated secrets
- secrets obfuscated or split so that no detector matches them; redaction handles matched multiline captures and PEM/PGP blocks
Attack Surface
- malicious config files: TOML parsing uses
serde_toml(memory-safe) - malicious git payloads: binary-safe parsing, control char stripping
- ReDoS via user regexes: 1 MB size limit enforced
- path traversal: canonicalization + prefix check in check-file mode
- terminal injection: sanitizes author/email/branch/file names before display
- agent-authored config: an in-workspace config layer is ignored on the agent-hook paths unless git-tracked and clean
- hostile hook payloads: bounded stdin and strict schemas; blocking hooks exit 2 on parse failure, while redaction returns stop JSON and a fully masked fallback when possible
Sandboxing
agent hook mode:
check-filereads only the target file (no directory traversal);check-codexreads no file at all, only the payload the agent is about to act on- no network access
- no temp file creation
- read-only access to config files
Future Work
Performance
- incremental scanning (cache results per file hash)
- rule priority ordering (check high-confidence rules first)
- streaming diff parsing (avoid buffering full diffs)
Features
- custom entropy models per rule (hex-only, base64-only)
- machine learning-based false positive reduction
- integration with secret management systems (vault, 1password)
Accuracy
- context-aware scanning (understand variable assignments)
- cross-file analysis (detect secrets split across imports)
- semantic analysis (distinguish API keys from UUIDs)