Traffic Light Protocol (TLP) file access control for AI coding tools. Enforces sensitivity-based access policies at the tool level — the AI never sees content it shouldn't.
CLAUDE.mdandAGENTS.mdare autogenerated by/Init(forge-init.sh). Do not edit directly — run/Update(forge-update.sh) to regenerate.
Behaviour — part of forge-core's three-layer architecture (Identity / Behaviour / Knowledge). Enforced via PreToolUse hook on every Read, Edit, and Write call.
Classifies files as RED (blocked), AMBER (requires approval), GREEN (open), or CLEAR (public) using a .tlp config file (YAML syntax) or frontmatter metadata. AMBER reads go through safe-read, which strips inline #tlp/red sections before the AI sees the content.
The convention is Obsidian-native — use the tlp: frontmatter property or the #tlp/red tag to redact sensitive sections inline.
| Skill | Purpose |
|---|---|
TLP |
TLP classification rules and access conventions |
RED:
- "*.pdf"
- "Resources/Contacts/**"
AMBER:
- "Resources/Journals/**"
GREEN:
- "Topics/**"
CLEAR:
- ".tlp"
- "CLAUDE.md"tlp-guard is a PreToolUse hook that intercepts Read, Edit, and Write tool calls before they execute. Claude Code pipes the tool call as JSON on stdin; the hook resolves the file's TLP level and either allows or blocks it.
Given the .tlp config above, Resources/Contacts/** is RED. When Claude tries to read a contact file, the hook blocks it:
$ echo '{"tool_name":"Read","tool_input":{"file_path":"/vault/Resources/Contacts/john.md"}}' \
| tlp-guard
TLP:RED — access blocked for: Resources/Contacts/john.md
$ echo $?
2Write and Edit calls are blocked the same way — no tool call targeting a RED file gets through:
$ echo '{"tool_name":"Edit","tool_input":{"file_path":"/vault/Resources/Contacts/john.md","old_string":"...","new_string":"..."}}' \
| tlp-guard
TLP:RED — access blocked for: Resources/Contacts/john.md
$ echo $?
2Exit code 2 tells Claude Code to deny the tool call. The AI never sees the file content.
For AMBER files that contain sensitive sections, safe-read strips #tlp/red regions before the AI sees the content.
Source file:
---
tlp: AMBER
---
# Meeting Notes
Discussed project timeline with the team.
#tlp/red
Salary negotiations: offered $150k, counter at $170k.
#tlp/amber
Next steps: finalize budget by Friday.
Contact Alice at alice@example.com #tlp/red (personal: 555-0123) #tlp/amber for details.safe-read output:
---
tlp: AMBER
---
# Meeting Notes
Discussed project timeline with the team.
[REDACTED]
Next steps: finalize budget by Friday.
Contact Alice at alice@example.com [REDACTED] for details.
Block-mode #tlp/red sections are replaced with [REDACTED]. Inline #tlp/red markers redact to the next #tlp/* boundary tag or end of line. Any detected secrets (API keys, tokens, credentials) are replaced with [SECRET REDACTED] using patterns sourced from gitleaks.
- tlp-guard (hook) — PreToolUse hook that intercepts Read/Edit/Write
- safe-read (CLI) — Reads files with inline
#tlp/redredaction + secret detection - blind-metadata (CLI) — Bulk YAML frontmatter operations
- Rust toolchain (rustup.rs) — binaries build on first use
- A
.tlpfile at the root of each directory tree to protect
/plugin marketplace add N4M3Z/forge-plugins
/plugin install forge-tlp@forge-plugins
claude --plugin-dir /path/to/Modules/forge-tlpWhitelist the CLI tools in your project or global settings.local.json:
{
"permissions": {
"allow": [
"Bash(<plugin-path>/bin/safe-read:*)",
"Bash(<plugin-path>/bin/blind-metadata:*)"
]
}
}Create a .tlp file at your directory root. See examples/tlp.example.yaml.
Patterns are listed under level headers (RED:, AMBER:, GREEN:, CLEAR:) as quoted strings with a - prefix:
| Pattern | Matches | Example |
|---|---|---|
*.ext |
Any file with that extension, anywhere in the tree | "*.pdf" matches docs/report.pdf |
dir/** |
All files under a directory (recursive) | "Contacts/**" matches Contacts/john.md |
exact/path.md |
Exact relative path only | "README.md" matches only README.md at the root |
First match wins. Files not matched by any pattern default to AMBER.
Files can escalate their own protection level via a tlp: field in YAML frontmatter:
---
tlp: RED
---The effective level is the more restrictive of the path-based and frontmatter-based classification. A file can escalate (GREEN path + RED frontmatter = RED) but never downgrade (AMBER path + GREEN frontmatter = AMBER).
If .tlp exists but cannot be read (permissions, corruption), all files in that vault are treated as RED and access is blocked until the config is fixed. This prevents accidental exposure from a broken config.
Files outside any vault (no .tlp in any parent directory) are not affected by the hook.
Read request
→ tlp-guard-wrapper.sh (builds if needed)
→ tlp-guard binary
→ walks up to .tlp config
→ classifies file (path pattern + frontmatter override)
→ RED: block (exit 2)
→ AMBER + Read: block, suggest safe-read
→ AMBER + Edit/Write: allow + warn
→ GREEN/CLEAR: allow
Hooks use bash scripts. Windows users need WSL or Git Bash. Claude Code plugin hooks don't currently support .bat/.ps1 natively.
safe-read also checks TLP classification and refuses RED files.
# Run all tests (unit + integration)
cargo test
# Check for warnings
cargo clippy -- -D warnings
# Build release binaries
cargo build --release
# Format code
cargo fmtsrc/
lib.rs # Library crate (re-exports modules)
tlp/
mod.rs # TLP enum, classify(), pattern matching
tests.rs # Unit tests
vault/
mod.rs # Vault discovery (walk up to .tlp)
tests.rs # Unit tests
redact/
mod.rs # TLP section redaction + secret detection
tests.rs # Unit tests
frontmatter/
mod.rs # YAML frontmatter get/set, .md file listing
tests.rs # Unit tests
bin/
tlp-guard.rs # PreToolUse hook binary
safe-read.rs # Redacting file reader binary
blind-metadata.rs # Frontmatter bulk operations binary
tests/
fixtures/
configs/ # .tlp config fixtures
content/ # .md content fixtures
tlp_guard.rs # Integration tests for tlp-guard
safe_read.rs # Integration tests for safe-read
blind_metadata.rs # Integration tests for blind-metadata
- Claude Code Hooks — PreToolUse
- gitleaks — secret detection patterns used by
safe-read
Use Core/bin/paths.sh as the shared cross-platform path contract.
From the repository root:
eval "$(bash Core/bin/paths.sh)"Then use the exported values (FORGE_ROOT, FORGE_USER_ROOT, SAFE_READ_CMD, SAFE_WRITE_CMD, memory paths, journal path, backlog path) instead of re-implementing path/env resolution inside skills.