$ For millions of years mankind lived just like animals. Then something happened which unleashed the power of our imagination.

nah: A Structural Permission Guard for Claude Code Tool Calls

claude-code, security, agentic-workflows, permissions, developer-tools

Claude Code’s built-in permission model operates on a per-tool allow-or-deny basis. nah, an open-source Python package by Manuel Schipper, replaces that with structural command classification — the same rm command gets different treatment depending on whether the target is inside the project directory or pointing at ~/.bashrc. It ships 20 built-in action types, each mapped to one of four default policies: allow, context-dependent, ask, or block.

The tool installs as a PreToolUse hook and intercepts calls to Bash, Read, Write, Edit, Glob, Grep, and MCP tools. Classification is deterministic and runs without any LLM calls. For ambiguous cases that fall into the “ask” bucket, an optional LLM layer (supporting Ollama, OpenRouter, OpenAI, Anthropic, and Snowflake Cortex) can resolve the decision — but it can never escalate past “ask” by default.

How Classification Works

Rather than maintaining deny-lists of specific commands, nah maps every tool call to an action type based on structural analysis: pipe composition, shell unwrapping, path sensitivity, and content inspection. The same binary (rm) resolves to different action types depending on context.

ScenarioContextDecision
rm dist/bundle.jsInside project directoryAllow
rm ~/.bashrcOutside project, sensitive pathAsk
git pushStandard pushAllow
git push --forceHistory rewriteAsk
base64 -d | bashDecode-to-exec pipeBlock
Read ./src/app.pyProject sourceAllow
Read ~/.ssh/id_rsaSensitive credentials pathBlock

Write and Edit tools get content inspection on top of path checks — a write containing -----BEGIN PRIVATE KEY----- triggers a flag regardless of the target path.

Configuration Model

Global config lives at ~/.config/nah/config.yaml. Per-project overrides (.nah.yaml) can add classifications and tighten policies but cannot relax them — a malicious repository cannot allowlist dangerous commands through its own config file.

Three built-in profiles control the starting rule set:

ProfileCoverage
full (default)Shell, git, packages, containers, network
minimalCurated essentials (rm, git, curl, kill)
noneBlank slate

CLI commands (nah allow, nah deny, nah classify) modify policies without editing YAML directly. nah test "rm -rf /" dry-runs classification for any command.

Practical Implications

For anyone running Claude Code with --dangerously-skip-permissions (or wanting to), this addresses the core tension: hooks in bypass mode fire asynchronously, meaning commands execute before guards can block them. The intended setup is to allow-list tools like Bash, Read, Glob, and Grep in Claude Code’s native permissions, then let nah handle the fine-grained decisions.

The project includes a built-in security demo (/nah-demo inside Claude Code) covering 25 test cases across 8 threat categories — remote code execution, data exfiltration, obfuscated commands, and others. Useful for validating the guard against your own workflow before trusting it in production.

For agentic setups where models chain tool calls autonomously, structural classification per-call is a more maintainable approach than curating static deny-lists that inevitably fall behind model capabilities.

References

  1. manuelschipper/nah — GitHub
  2. Claude Code Hooks documentation — Anthropic
  3. Bypass mode async hook issue — GitHub #20946

---

Configuration details reflect a production environment at time of writing. Implementation specifics vary based on tooling versions, platform updates, and organizational requirements. Validate approaches against current documentation before deployment.