Back to Articles

nah: A Context-Aware Permission System That Makes Claude Code Actually Safe

[ View on GitHub ]

nah: A Context-Aware Permission System That Makes Claude Code Actually Safe

Hook

Claude Code can rewrite your git history, delete system files, and exfiltrate credentials—all while you’re reviewing what looks like a harmless bug fix. Simple allow/deny lists won’t save you.

Context

AI coding assistants have evolved from autocomplete suggestions to autonomous agents that execute shell commands, modify files, and interact with APIs. Claude Code represents this new generation: give it a task, and it writes code, runs tests, commits changes, even deploys to production. The productivity gains are remarkable, but the security model is binary: either you review every single command (exhausting), or you blanket-allow tools and hope Claude doesn’t hallucinate a rm -rf / while refactoring your test suite.

The fundamental problem is that traditional permission systems think in terms of tools—“allow bash” or “deny curl”—but AI agents think in terms of intent. The same git command can be harmless (viewing logs) or destructive (rewriting commit history to remove security patches). You don’t want to block git entirely, but you also don’t want Claude force-pushing to main at 2 AM because it misunderstood your merge conflict instructions. What’s missing is a permission layer that understands context and intent, not just command names.

Technical Insight

tool call

raw command

parsed structure

action type

baseline rules

tightened rules only

merged policy

deterministic

deterministic

ambiguous

context/ask

classification

user decision

user decision

Claude Code Editor

PreToolUse Hook Interceptor

Command Parser & Tokenizer

Action Type Classifier

20+ categories

Two-Tier Config Engine

Global Config

~/.config/nah

Project Config

.nah.yaml

Policy Decision Engine

LLM Classifier

Ollama/OpenRouter/etc

Execute Command

Block Command

Prompt Human

System architecture — auto-generated

nah hooks into Claude Code’s PreToolUse lifecycle as an interceptor—every command passes through its classifier before execution. Instead of matching command names against lists, it parses commands into a 20+ action taxonomy that captures intent: filesystem_delete, git_history_rewrite, network_external, process_background, obfuscated, and more. This structural approach is what makes it resilient.

Here’s how the classification works in practice:

# These three commands all delete files, but nah treats them differently:

# 1. Deleting a temp file in your project → ALLOW (instant)
rm /tmp/my-project-cache.txt

# 2. Deleting from system directories → BLOCK (instant)
rm /etc/hosts

# 3. Deleting files with Git → ASK (needs human judgment)
git filter-branch --tree-filter 'rm -f passwords.txt' HEAD

The classifier first extracts syntactic structure—is this a pipeline, a subshell, a redirect? Then it identifies the core action type. For the rm examples above, it’s not just seeing “rm” and blocking everything. It’s checking: is the path inside the project boundary? Does it match system-critical patterns? Is it wrapped in a git history-rewriting command?

The two-tier configuration system is where nah gets clever about supply-chain attacks. Your global config lives in ~/.config/nah/config.yaml and sets baseline security. Project-specific .nah.yaml files can only tighten restrictions, never loosen them:

# ~/.config/nah/config.yaml (global defaults)
actions:
  filesystem_delete:
    policy: context  # Ask when deleting outside project
  git_history_rewrite:
    policy: ask      # Always confirm history rewrites
  network_external:
    policy: block    # No external network calls

# .nah.yaml (project-specific, can only tighten)
actions:
  filesystem_delete:
    policy: ask      # Even stricter: ask for ALL deletes
  # Can't set network_external to 'allow'—would be ignored

This hierarchy prevents a malicious repository from shipping a .nah.yaml that says “allow everything”—your global config acts as a security floor. It’s the same principle as Content Security Policy in browsers: defaults deny, exceptions are explicit, and untrusted sources can’t grant themselves permissions.

For genuinely ambiguous commands, nah can cascade to an LLM as a second opinion. This isn’t about using AI to guard AI—it’s about handling the long tail of edge cases that deterministic rules miss:

# Deterministic classifier sees: command substitution + network + eval
curl https://install.sh | bash
# → Classified as 'obfuscated' + 'network_external' → BLOCK (no LLM needed)

# But this is trickier:
find . -name '*.pyc' -delete
# → 'filesystem_delete' but context unclear (build artifacts? or backdoor cleanup?)
# → Cascade to LLM: "Is deleting all .pyc files safe in this context?"

The LLM layer is optional and configurable—you can point it at Ollama for local inference, OpenRouter for model variety, or disable it entirely and fall back to human prompts. The key architectural decision is that 90%+ of decisions happen deterministically in milliseconds. The LLM is an escape hatch for ambiguity, not the primary decision-maker.

Taxonomy profiles let you dial complexity up or down. The ‘minimal’ profile collapses to just 5 action types (filesystem, network, git, process, system), while ‘full’ expands to 20+ with fine-grained distinctions between git_history_rewrite and git_commit. You can even define custom action types:

taxonomy: full
custom_actions:
  credential_access:
    patterns:
      - 'cat.*\.env'
      - 'echo.*AWS_SECRET'
      - 'printenv.*TOKEN'
    policy: block

This pattern-based extension mechanism means you can encode your organization’s specific security rules—maybe you always block kubectl commands against production namespaces, or flag any script that touches /var/log/audit.

Gotcha

nah only works in Claude Code’s default permission mode. If you or a teammate runs with --dangerously-skip-permissions, the hooks fire asynchronously after commands execute, turning nah into a logger instead of a guard. There’s no way around this—it’s a fundamental limitation of the hook architecture. You need to enforce the runtime flag through team policy or wrapper scripts.

The action taxonomy is comprehensive but not omniscient. Novel obfuscation techniques or attack patterns won’t be caught until the rule set is updated. An attacker who knows nah’s classification rules could theoretically craft commands that slip through—think of it like antivirus signatures. The deterministic classifier is fast and reliable for known patterns, but it’s not doing deep semantic analysis. That’s why the LLM cascade exists, but even LLMs can be prompt-injected or confused.

Latency becomes noticeable when the LLM layer activates frequently. If your workflow triggers lots of ambiguous commands, you’ll feel the 1-3 second delays while Ollama or OpenRouter evaluates them. You can tune the escalation threshold to reduce this, but then you’re trading security for speed. The sweet spot depends on your risk tolerance and how predictable your Claude workflows are.

Verdict

Use if: You’re running Claude Code on sensitive infrastructure (dotfiles, production configs, systems with API keys), you want to grant Claude more autonomy without constant supervision, or you’re experimenting with aggressive task delegation and need guardrails. The context-aware classification prevents both permission fatigue and catastrophic mistakes. Skip if: You’re working in disposable sandboxes or containers where Claude can’t hurt anything, you need absolute zero-latency command execution (deterministic decisions are fast, but not instantaneous), or your team uses --dangerously-skip-permissions mode where nah fundamentally cannot block commands. For production use with AI coding assistants, this is the permission model that should be standard—it’s what binary allow/deny should have been from the start.

// ADD TO YOUR README
[![Featured on Starlog](https://starlog.is/api/badge/ai-dev-tools/manuelschipper-nah.svg)](https://starlog.is/api/badge-click/ai-dev-tools/manuelschipper-nah)