Welcome to the developer documentation for the Irreversible Command Gate (icg). This guide explains how to extend icg with new rule packs, contribute to the core engine, and understand the system architecture.
- Architecture Overview
- Development Environment
- Understanding Rule Packs
- Creating a New Rule Pack
- Front-End Integration
- Testing and Validation
- Release Process
- Code Organization
- Common Patterns
icg consists of several key components:
┌─────────────────────────────────────────────────────────────┐
│ AI Agent │
└───────────────────────────┬─────────────────────────────────┘
│ attempts operation
▼
┌─────────────────────────────────────────────────────────────┐
│ Front-End Layer │
│ ┌──────────────────────┐ ┌──────────────────────────┐ │
│ │ Claude Code Hook │ │ Codex CLI Hook │ │
│ │ (PreToolUse JSON) │ │ (PreToolUse JSON) │ │
│ └──────────────────────┘ └──────────────────────────┘ │
│ ┌──────────────────────┐ │
│ │ PATH Wrapper │ │
│ │ (Symlink shadows) │ │
│ └──────────────────────┘ │
└───────────────────────────┬─────────────────────────────────┘
│ parsed input
▼
┌─────────────────────────────────────────────────────────────┐
│ Evaluation Engine │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Input Parser (command-mode & content-mode) │ │
│ └──────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Pack Dispatcher (matches tool_keywords/applies_to) │ │
│ └──────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Pattern Evaluator (safe_patterns → guarded_patterns) │ │
│ └──────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Redirect Handler (deny/updated_input/context) │ │
│ └──────────────────────────────────────────────────────┘ │
└───────────────────────────┬─────────────────────────────────┘
│ decision + redirect
▼
┌─────────────────────────────────────────────────────────────┐
│ Output Layer │
│ ┌──────────────────────┐ ┌──────────────────────────┐ │
│ │ Structured Denial │ │ Telemetry/Logging │ │
│ │ (JSON to stdout) │ │ (Denial log, metrics) │ │
│ └──────────────────────┘ └──────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
-
Fail-Open by Default: Any parse error or exception allows the operation to proceed
- A missed violation is recoverable; a stuck fleet is not
- Only the guard process crashing graduates to fail-closed after reliability validation
-
Zero Network I/O: Core evaluation doesn't make network calls
- Ensures deterministic behavior
- Prevents cascading failures
- Exception:
git pushstale-HEAD check (already a network operation)
-
Modular Rule Packs: Each tool gets its own pack
- Easy to add new rules without touching core code
- Clear separation of concerns
- Per-tool release cycles
-
Two-Frontend Design:
- Hook Frontend: Claude Code & Codex CLI (PreToolUse JSON)
- Wrapper Frontend: Symlink shadows in
$PATH
-
Redirect-Not-Just-Block: Every denial explains what to do instead
reason_template: Why it's blockedrewrite_template: Safe alternative (when available)channel: deny, updated_input, or additional_context
- Rust: 1.70+ (2021 edition)
- Cargo: Built-in build system
- Git: For version control
- jq: For JSON testing (optional but recommended)
# Clone the repository
git clone https://github.com/jedarden/irreversible-command-gate.git
cd irreversible-command-gate
# Verify dependencies
cargo check
# Run tests
cargo test
# Build the binary
cargo build --releaseirreversible-command-gate/
├── src/
│ ├── main.rs # CLI entry point, command routing
│ ├── lib.rs # Library exports
│ ├── engine.rs # Core evaluation engine
│ ├── rule_pack.rs # Rule pack schema and loader
│ ├── state_store.rs # Persistent state (Phase 2)
│ ├── telemetry.rs # Metrics and logging
│ ├── health.rs # Health checks
│ ├── denial_log.rs # Denial history
│ ├── overrides.rs # Per-repository overrides
│ ├── regression.rs # Regression suite generation
│ ├── new_pack.rs # New rule pack scaffolding
│ ├── update.rs # Rule pack update system
│ └── trust_pointer.rs # Trust on first use (TOFU) infrastructure
├── tests/
│ └── fixtures/ # Test rule pack fixtures
├── docs/
│ ├── developers/ # This documentation
│ ├── operators/ # Operator guides
│ ├── notes/ # Design decisions
│ ├── research/ # Prior art
│ └── plan/ # Implementation roadmap
└── Cargo.toml # Rust dependencies
A rule pack is a JSON file defining patterns for a specific tool or domain:
{
"id": "pack-id",
"tool_keywords": ["tool1", "tool2"],
"applies_to": ["*.yaml", "*.yml"],
"safe_patterns": [...],
"guarded_patterns": [...]
}Command-Mode Packs (inspect shell invocations):
- Use
tool_keywordsto match executables - Use
command_regexfor pattern matching - Examples:
openbao,git,misc,tmux - Work in both hook and wrapper frontends
Content-Mode Packs (inspect file writes):
- Use
applies_toglobs to match file paths - Use
content_regexfor pattern matching - Examples:
storage-class,image-tag,beads - Hook-frontend only (Write/Edit never reaches wrapper)
Hybrid Packs:
secretspack: Hook-only (scans entire Bash command string)- Uses
command_regexbut unconditionally (no tool_keywords filter)
Both examples below are abridged from the shipped openbao pack — compare
against packs/openbao.json. Note that one pack covers both CLI names:
tool_keywords lists bao and vault.
{
"id": "safe-bao-kv-metadata-get",
"type": "command_regex",
"regex": "(?i)\\b(bao|vault)\\s+kv\\s+metadata\\s+get\\b"
}{
"id": "openbao-destructive-verb",
"type": "command_regex",
"regex": "(?i)\\b(bao|vault)\\s+(kv\\s+destroy|kv\\s+metadata\\s+delete|...)",
"tier": "tier1",
"severity": "Critical",
"explanation": "Permanently destroys secret data, an auth mount, a policy, or the unseal shares.",
"destructive": true,
"redirect": {
"channel": "deny",
"reason_template": "This is an irreversible OpenBao operation. 'kv delete' soft-deletes and is recoverable; 'kv destroy' and 'kv metadata delete' are not. ...",
"rewrite_template": null
}
}-
CommandRegex: Match against shell command tokens
{ "type": "command_regex", "regex": "git push.*--force" } -
ContentRegex: Match against file content
{ "type": "content_regex", "regex": "storageClassName:.*ssd" } -
Predicate: Custom check function (future)
{ "type": "predicate", "predicate_name": "is_shared_checkout" }
- Critical: Immediate, irreversible damage (
bao kv destroy,git push --force) - High: Significant damage or hard to reverse (policy delete, ssd storage)
- Medium: Moderate damage with workarounds
- Deny: Block the operation entirely (critical/high severity)
- UpdatedInput: Provide a safe alternative (future feature)
- AdditionalContext: Warn without blocking (Tier 3 patterns only)
Determine what you're protecting:
- Tool: Command-mode pack (e.g.,
kubectl,docker) - File format: Content-mode pack (e.g.,
terraform,cloudformation) - Domain: Hybrid pack (e.g.,
secrets,beads)
Use the built-in scaffolding tool:
cargo run --new-pack \
--id "kubectl" \
--mode command \
--keywords "kubectl,kubecfg"Or manually create the JSON:
# Create pack directory
mkdir -p packs/kubectl
# Create pack manifest
cat > packs/kubectl/pack.json <<'EOF'
{
"id": "kubectl",
"tool_keywords": ["kubectl", "kubecfg"],
"applies_to": [],
"safe_patterns": [],
"guarded_patterns": []
}
EOFList operations that should always be allowed:
{
"safe_patterns": [
{
"id": "safe-get",
"type": "command_regex",
"regex": "kubectl get"
},
{
"id": "safe-describe",
"type": "command_regex",
"regex": "kubectl describe"
},
{
"id": "safe-logs",
"type": "command_regex",
"regex": "kubectl logs"
}
]
}Best Practices for Safe Patterns:
- Start specific, relax gradually
- Use
^and$anchors for exact matches - Test against real command sequences
- Consider command chaining (
&&,||,;)
Identify dangerous operations:
{
"guarded_patterns": [
{
"id": "kubectl-delete-deployment",
"type": "command_regex",
"regex": "kubectl delete deployment",
"tier": "tier1",
"severity": "High",
"explanation": "Deleting a deployment removes all running pods",
"destructive": true,
"redirect": {
"channel": "deny",
"reason_template": "kubectl delete deployment is destructive. Use 'kubectl scale deployment --replicas=0' instead to preserve the deployment object.",
"rewrite_template": null
}
}
]
}Best Practices for Guarded Patterns:
- Start narrow, expand coverage iteratively
- Every pattern needs a clear explanation
- Provide alternatives when possible
- Mark as
destructive: trueif it causes data loss - Use
tier: "tier1"for stateless checks (Phase 1)
Create test cases for your patterns:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_kubectl_safe_patterns() {
let pack = load_pack("packs/kubectl/pack.json").unwrap();
// Test safe operations
assert!(pack.allows("kubectl get pods"));
assert!(pack.allows("kubectl describe deployment myapp"));
assert!(pack.allows("kubectl logs -f pod/mypod"));
}
#[test]
fn test_kubectl_guarded_patterns() {
let pack = load_pack("packs/kubectl/pack.json").unwrap();
// Test dangerous operations
assert!(pack.blocks("kubectl delete deployment myapp"));
}
}Generate a regression suite for CI:
cargo run --bin icg -- regression-suite \
packs/kubectl/pack.json \
--output tests/fixtures/kubectl-regression.jsonThis generates one validated deny case per enabled guarded_pattern.
Test the pack before integrating:
# Test specific command
cargo run --bin icg -- check \
--command "kubectl delete deployment myapp" \
--pack packs/kubectl/pack.json
# Run full test suite
cargo test
# Run integration tests
cargo test --test integrationCreate documentation for operators:
# kubectl Pack
## Overview
Protects against destructive kubectl operations.
## Safe Operations
- `kubectl get` - Read resources
- `kubectl describe` - View resource details
- `kubectl logs` - View pod logs
## Protected Operations
- `kubectl delete deployment` - Deletes deployments
- `kubectl delete svc` - Deletes services
## Severity Levels
- High: Deleting managed resources
- Medium: Deleting unmanaged resourcesThe hook frontend integrates with Claude Code and Codex CLI via the PreToolUse JSON interface.
- Agent calls a tool (Bash, Write, Edit, apply_patch)
- Harness sends PreToolUse JSON to hook stdin
- icg parses JSON, evaluates against rule packs
- icg outputs JSON decision to stdout
- Harness reads decision, blocks or allows the operation
Claude Code sends tool_name/tool_input; icg also accepts the camelCase
spelling used by older fixtures. Extra fields are ignored.
{
"tool_name": "Bash",
"tool_input": { "command": "bao kv destroy secret/test" },
"tool_use_id": "toolu_0123456789"
}Write and Edit supply file_path and content instead, feeding the
content-mode packs; Codex apply_patch input is accepted too, including
multi-file patches.
One decision envelope on stdout, matching the harness PreToolUse schema.
There is no icg-specific wrapper object.
Deny — the pack and pattern are appended to the reason, in brackets:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "This is an irreversible OpenBao operation. 'kv delete' soft-deletes and is recoverable; 'kv destroy' and 'kv metadata delete' are not. ... [pack=openbao, pattern=openbao-destructive-verb]"
}
}Rewrite (updated_input channel) — an allow that hands the harness a
safe command to retry with:
{
"hookSpecificOutput": {
"additionalContext": "Removed --force/-f/--force-with-lease from git push; ... [pack=git, pattern=git-force-push]",
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"updatedInput": { "command": "git push origin main" }
}
}Warning (additional_context channel) — an allow that carries a caution:
{
"hookSpecificOutput": {
"additionalContext": "This read prints a secret value to stdout ... [pack=openbao, pattern=openbao-kv-get-to-stdout]",
"hookEventName": "PreToolUse",
"permissionDecision": "allow"
}
}Allow — nothing matched:
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}Reproduce any of these:
echo '{"tool_name":"Bash","tool_input":{"command":"bao kv destroy secret/test"}}' \
| icg hook --rule-pack packsThe wrapper frontend shadows binaries via symlinks in $PATH.
- Agent runs
bao kv destroy secret/test - Shell resolves
$PATHto the icg symlink at/usr/local/bin/bao - icg intercepts argv, evaluates against rule packs
- icg writes the decision to stderr
- icg execs the real
baofound later in$PATHif allowed - icg exits non-zero without exec'ing if denied
This does not cover absolute-path invocations (/usr/bin/bao ...) or direct
library calls. The hook front-end is the complete one.
icg install creates the symlinks for every command-mode pack currently
loaded, into /usr/local/bin by default:
sudo icg install # or --dir <path>, --uninstall to removeIt derives the names from the packs' tool_keywords, so the set tracks the
installed policy rather than a hand-maintained list. There is no kubectl
symlink and there will not be one — kubectl stays with the org-level hook.
icg knows it's in wrapper mode when:
argv[0]is a symlink to the icg binary- Symlink basename matches a tool_keyword in some pack
To add support for a new AI harness (e.g., a future Codex variant):
-
Define the Hook Interface:
// src/adapters/new_harness.rs pub struct NewHarnessAdapter; impl HarnessAdapter for NewHarnessAdapter { fn parse_input(&self, stdin: &str) -> Result<Input> { // Parse harness-specific JSON format } fn format_output(&self, decision: &Decision) -> String { // Format decision for harness consumption } }
-
Register the Adapter:
// src/main.rs match harness_type { "claude-code" => ClaudeCodeAdapter, "codex-cli" => CodexAdapter, "new-harness" => NewHarnessAdapter, _ => return Err(anyhow!("Unknown harness")), }
-
Add Tests:
#[test] fn test_new_harness_adapter() { let adapter = NewHarnessAdapter; let input = /* harness-specific input */; let decision = adapter.evaluate(&input).unwrap(); assert_eq!(decision.verdict, Verdict::Deny); }
Test individual components:
# Run all unit tests
cargo test
# Run specific test
cargo test test_pattern_matching
# Run with output
cargo test -- --nocapture
# Run tests matching a pattern
cargo test pack::An operational denial write resolves its sink through
denial_log::operational_log_path. With ICG_DENIAL_LOG unset that is the
host's live log, /var/cache/icg/denials.jsonl — and a test-driven process is
refused it outright: the denial is evaluated normally, nothing is recorded, and
nothing is printed. This keeps cargo test from feeding fixture denials into a
log an instrumented host is collecting real traffic into, where a fixture that
exercises a real pattern id is indistinguishable from a live denial after the
fact.
A test that asserts on recorded denials names its own sink:
.env("ICG_DENIAL_LOG", temp.path().join("denials.jsonl"))The guard covers both process shapes cargo test produces: this crate's own
test binaries, and the unmodified icg binary an integration test spawns
through CARGO_BIN_EXE_icg. tests/denial_log_pollution_guard_tests.rs fails
if either ever reaches the live log.
Test end-to-end workflows:
# Run integration tests
cargo test --test integration
# Run with specific rule pack
ICG_PACK_DIR=./packs/kubectl/pack.json cargo testValidate that destructive patterns remain protected:
# Generate regression suite
cargo run --bin icg -- regression-suite \
packs/kubectl/pack.json \
--output kubectl-regression.json
# Run regression tests
cargo test --test regression
# Verify no coverage narrowing
cargo run --bin icg -- verify-coverage \
--current kubectl-regression.json \
--previous previous-kubectl-regression.jsonTest with real commands:
# Test a specific command
ICG_PACK_DIR=./packs/openbao.json \
cargo run --bin icg -- check \
--command "vault kv destroy secret/test"
# Test with hook input
echo '{"toolName":"Bash","toolInput":{"command":"vault kv destroy secret/test"}}' | \
cargo run --bin icg -- check --stdin
# Test in wrapper mode
ln -sf $(cargo root)/target/release/icg /tmp/vault
/tmp/vault kv destroy secret/testMeasure evaluation latency:
# Benchmark evaluation
cargo bench --bench evaluation
# Profile hot paths
cargo flamegraph --bin icg -- check \
--command "vault kv get secret/test"
# Check memory usage
valgrind --tool=massif \
cargo run --bin icg -- check \
--command "git log --oneline"icg follows Semantic Versioning:
- Major: Breaking changes to rule pack schema or evaluation engine
- Minor: New features, new rule packs
- Patch: Bug fixes, documentation updates
-
Update Version:
# Update Cargo.toml version = "0.2.0"
-
Run Full Test Suite:
cargo test --all-features cargo clippy --all-targets cargo fmt --check -
Generate Regression Suite:
cargo run --bin icg -- regression-suite \ packs/*.json \ --output regression-suite.json -
Build Release Binary:
cargo build --release
-
Create Release Notes:
## Release v0.2.0 (2026-08-16) ### Added - kubectl rule pack (destructive operations) - Terraform content-mode pack - Regression suite generation CLI ### Changed - Improved error messages for pattern matching - Updated documentation ### Fixed - Fixed false positive in git force-push detection
-
Tag and Push:
git tag -a v0.2.0 -m "Release v0.2.0" git push origin v0.2.0 -
Publish to GitHub:
- Create GitHub Release
- Upload binary artifacts
- Attach regression suite
main.rs: CLI entry point, command routingengine.rs: Core evaluation logic, pattern matchingrule_pack.rs: Rule pack schema, serializationstate_store.rs: Persistent state (session history, Phase 2)telemetry.rs: Metrics, denial logginghealth.rs: Health check endpointsdenial_log.rs: Denial history and trend analysisoverrides.rs: Per-repository overridesregression.rs: Regression suite generation and validationnew_pack.rs: Rule pack scaffolding CLIupdate.rs: Rule pack update systemtrust_pointer.rs: TOFU infrastructure for rule pack updates
-
Create the module file:
touch src/my_module.rs
-
Export from lib.rs:
// src/lib.rs pub mod my_module;
-
Write tests:
// src/my_module.rs #[cfg(test)] mod tests { use super::*; #[test] fn test_my_function() { // Test implementation } }
-
Document public API:
/// Performs a specific operation /// /// # Arguments /// /// * `input` - The input to process /// /// # Returns /// /// Result containing the output or an error pub fn my_function(input: &str) -> Result<String> { // Implementation }
Match shell command tokens:
use regex::Regex;
fn match_command_regex(pattern: &str, command: &str) -> bool {
let regex = Regex::new(pattern).unwrap();
regex.is_match(command)
}Match file content being written:
fn match_content_regex(pattern: &str, content: &str) -> bool {
let regex = Regex::new(pattern).unwrap();
regex.is_match(content)
}Match file paths against globs:
fn matches_glob(path: &str, glob: &str) -> bool {
// Normalize paths
let path = path.replace('\\', "/");
let glob = glob.replace('\\", "/");
// Handle simple globs (*)
if glob.contains('*') {
let parts: Vec<&str> = glob.split('*').collect();
// Check each part matches
}
// Handle recursive globs (**)
// Handle path separators
// Handle relative vs absolute paths
}Custom check functions (future):
fn evaluate_predicate(name: &str, context: &Context) -> bool {
match name {
"is_shared_checkout" => {
// Check if .git is a directory
std::path::Path::new(".git").is_dir()
}
"has_staged_changes" => {
// Run git diff --cached
Command::new("git")
.args(&["diff", "--cached"])
.output()
.map(|o| !o.stdout.is_empty())
.unwrap_or(false)
}
_ => false,
}
}- Fork the repository
- Create a feature branch:
git checkout -b feature/my-rule-pack
- Make your changes
- Add tests
- Update documentation
- Submit a pull request
All contributions go through code review:
- Automated Checks: CI runs tests, clippy, fmt
- Peer Review: Another developer reviews your changes
- Architecture Review: For significant changes
- Documentation Review: Ensure docs are updated
- Rust 2021 Edition
- Use
Resultfor errors: Never silently fail - Document public APIs: All public functions need rustdoc
- Write tests: Aim for >80% coverage
- Format code: Use
cargo fmt - Lint: Pass
cargo clippy
- Operator Documentation:
docs/operators/README.md - Architecture Plan:
docs/plan/plan.md - Design Notes:
docs/notes/ - GitHub Issues: https://github.com/jedarden/irreversible-command-gate/issues
- Search existing issues first
- Create a minimal reproduction for bugs
- Include context: icg version, OS, rule pack version
- Be specific: What you tried, what you expected, what happened
Developer Documentation Version: 1.0 Last Updated: 2026-08-16 For: icg v0.1.0+