nah: A Structural Permission Guard for Claude Code Tool Calls
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.
| Scenario | Context | Decision |
|---|---|---|
rm dist/bundle.js | Inside project directory | Allow |
rm ~/.bashrc | Outside project, sensitive path | Ask |
git push | Standard push | Allow |
git push --force | History rewrite | Ask |
base64 -d | bash | Decode-to-exec pipe | Block |
Read ./src/app.py | Project source | Allow |
Read ~/.ssh/id_rsa | Sensitive credentials path | Block |
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:
| Profile | Coverage |
|---|---|
full (default) | Shell, git, packages, containers, network |
minimal | Curated essentials (rm, git, curl, kill) |
none | Blank 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
- manuelschipper/nah â GitHub
- Claude Code Hooks documentation â Anthropic
- 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.