> your AI agent picks dependencies from memory; give it dated facts — try starlog.dev ↗ vet your agent's deps ↗ vibe-coding is fine. vibe-importing isn’t. — try starlog.dev ↗ vibe-importing isn’t fine ↗ your agent has never seen your private packages — try starlog.dev ↗ facts for private packages ↗ a linter for the dependencies your AI agent picks — try starlog.dev ↗ a linter for agent deps ↗

Back to Articles

nah: Deterministic Permission Guards for AI Coding Agents

[ View on GitHub ]

nah: Deterministic Permission Guards for AI Coding Agents

Hook

Your AI coding agent just tried to run git filter-branch --force to rewrite history across your entire repository. Did you mean to approve that, or did you think it was just viewing git logs?

Context

AI coding agents like Claude Code and GitHub Copilot Workspace have fundamentally changed how developers work. They can scaffold entire projects, refactor codebases, and execute complex shell commands—all with minimal human intervention. But this power creates a critical trust problem: when you approve a command, you're often approving based on incomplete context or a quick glance at what looks like a benign operation.

Traditional safety approaches fall into two camps, both inadequate. The first is command-name allowlisting: block dangerous binaries like rm or curl, allow safe ones like ls or git. This fails immediately because git can be both harmless (git log) and catastrophic (git push --force --delete origin main). The second is LLM-based self-moderation: ask the AI to judge whether its own command is safe. This burns tokens, adds latency, and fundamentally relies on the same model that generated the risky command to assess its own risk. nah introduces a third path: deterministic action-intent classification that runs in milliseconds, requires no LLM calls, and makes safety decisions based on what a command actually does, not just what binary it invokes.

Technical Insight

At its core, nah is a command interceptor that hooks into agent execution flows before commands reach the shell. When Claude Code or Codex wants to execute a command, nah captures it, parses the full command string including arguments and flags, and classifies it into an action-intent taxonomy. This taxonomy includes categories like git_history_rewrite, filesystem_delete_recursive, network_outbound_curl, and secret_exfiltration—each mapped to policies that can allow, prompt, or block.

The architecture is deliberately minimal. The core classifier is a pure Python module with zero required dependencies, using pattern matching and contextual analysis rather than complex parsing libraries. Here's how a typical classification flow works:

# Example: Classifying a git command
command = "git push --force origin main"

# nah's classifier extracts:
# - base_command: "git"
# - subcommand: "push"
# - danger_flags: ["--force"]
# - context: remote operation on protected branch

# Classification result:
action = "git_force_push"
policy = "ask"  # Based on default ruleset
reason = "Force push detected to remote branch 'main'"

The power lies in how nah handles context. A command like rm -rf is classified differently based on its target: rm -rf ./tmp might auto-approve as temp_cleanup, while rm -rf / or rm -rf ~ triggers filesystem_delete_catastrophic and blocks immediately. Similarly, curl https://api.internal.company gets flagged as network_outbound_corp (ask), while curl https://pastebin.com might trigger potential_exfiltration (block) if combined with piped sensitive data.

The threat modeling is extensive. nah includes 1,807 test cases covering 13 danger classes, including sophisticated evasion attempts. It catches wrapper evasion (sh -c 'rm -rf /'), obfuscated commands ($(echo 'rm -rf' | base64 -d)), and guard tampering attempts (unalias nah && rm -rf /). Each test case is deterministic—run the same command twice, get the same classification, in the same ~2ms timeframe.

Configuration uses a progressive disclosure model. Out of the box, nah applies sensible defaults: block destructive operations, ask for network calls and git remote operations, auto-approve reads and safe writes. For project-specific needs, you define policies in .nah.yaml:

# Tighten network restrictions for a security-critical project
policies:
  network_outbound_curl:
    action: block
    reason: "No outbound HTTP in production deployment scripts"
  
  # But allow internal package registry
  custom_classifiers:
    - pattern: "curl https://registry\\.company\\.internal/.*"
      action: "package_fetch"
      policy: allow

# Custom action for your specific tooling
  - pattern: "./deploy\\.sh --prod"
    action: "production_deploy"
    policy: ask
    prompt: "Deploy to PRODUCTION environment?"

Integration with agents happens at multiple layers. For Claude Code, nah hooks into the tool approval surface that Claude Desktop exposes. For shell-based agents or direct usage, it modifies your .bashrc or .zshrc to intercept commands before execution. The key architectural decision here is runtime-agnostic classification: the same core engine works whether you're using Claude's MCP protocol, a custom Codex wrapper, or direct shell commands.

One particularly clever aspect is how nah handles ambiguous commands. If the deterministic classifier returns low confidence (command patterns it hasn't seen, unusual flag combinations), it can optionally defer to an LLM for judgment—but only as a fallback. This hybrid approach means 95%+ of commands resolve in milliseconds deterministically, and only truly novel cases burn tokens for LLM analysis. You maintain speed and predictability while still handling edge cases gracefully.

Gotcha

The most significant limitation is coverage dependency on runtime cooperation. If you run Claude Code with --dangerously-skip-permissions, nah never sees the commands. It can only intercept what the agent runtime exposes through approval hooks or what passes through shell execution. For agents that bundle commands into opaque scripts before execution, nah's visibility is limited to the outer script invocation, not the commands within.

Deterministic classification trades comprehensiveness for speed and predictability, which means false positives are inevitable with unusual workflows. If your project uses a custom deployment script that happens to match nah's pattern for filesystem_delete_recursive, you'll get prompted every time—even if it's perfectly safe in your context. The solution is custom classifiers in .nah.yaml, but maintaining these becomes another configuration burden, especially for teams with many specialized tools. The 1,807 test cases cover common evasion techniques, but sufficiently obfuscated or novel command structures could theoretically slip through pattern matching, though the extensive threat modeling makes this increasingly difficult.

Verdict

Use nah if you're running AI coding agents on production codebases or sensitive projects where you need auditable, deterministic safety without trusting LLM self-moderation. It's particularly valuable for teams that want reproducible security policies across developers—everyone gets the same classifications for the same commands, and policy changes are versioned in .nah.yaml alongside code. The millisecond response time means it disappears into your workflow rather than interrupting it. Skip it if you primarily use agents in sandbox environments where mistakes are cheap, if your agent already runs in OS-level containers that provide sufficient isolation, or if you're willing to manually review every single command (nah's value is intelligent filtering, not blanket blocking). Also skip if your agent ecosystem doesn't expose approval hooks that nah can intercept—check compatibility with your specific tools first.