Taint Tracking¶
Taint tracking is a session-level flow layer. Normal nah policy still controls the immediate tool call. When taint is enabled, nah can also remember that a session successfully read a sensitive source and then flag later side effects that might activate or move that data.
The goal is not full program analysis. It is a deterministic guardrail for read -> write -> execute/network/remote chains that are hard to judge one tool call at a time.
Mental Model¶
| Term | Meaning |
|---|---|
| Source | A configured sensitive path, or an inherited sensitive path, was read successfully. |
| Label | A user-defined name attached to a source, such as secret, customer_data, or prod_config. |
| Propagation | Tainted state was written into a trackable target, so that target inherits the label. |
| Activation | Code or agent execution after taint exists in the session. |
| Boundary | A remote, network, database, browser-state, service, or git-remote action after taint exists. |
| Unknown | An unrecognized action after taint exists. In v1 this remains at least ask. |
A blocked source access does not taint the session. nah only tracks sources that were allowed and, for runtimes with post-tool hooks, confirmed as executed.
Runtime Scope¶
| Runtime | Taint behavior |
|---|---|
| Claude Code | Uses PreToolUse and post-tool hooks for source tracking, execution confirmation, and enforcement. |
| Codex | Uses PreToolUse/PostToolUse for observation and PermissionRequest for enforceable decisions. Review hooks in /hooks after install or upgrade. |
| Terminal Guard | Audit-only in v1. It can log taint findings, but taint policy does not change terminal decisions. |
Enable Audit Mode¶
Start with audit mode. It records what nah would have asked or blocked without changing the runtime decision.
# ~/.config/nah/config.yaml
taint:
mode: audit
Then inspect decisions:
nah log --json
Example log metadata:
{
"taint": {
"mode": "audit",
"labels": ["secret"],
"chain": "Read ~/.aws/credentials secret -> Bash curl -I https://example.com",
"category": "boundary",
"policy": "secret + boundary = ask",
"policy_decision": "ask",
"would_decision": "ask",
"enforced": false
}
}
Enforce Mode¶
When the audit output matches the workflow you want, switch to enforce mode. Only policies stricter than the original decision can change the outcome.
taint:
mode: enforce
For example, if a command would normally be allowed but follows a sensitive read
and matches a boundary: ask policy, nah can escalate it to ask. If the
normal classifier already asked, nah records taint context rather than making
the decision weaker.
Sources¶
By default, when taint tracking is enabled, nah inherits effective sensitive paths as taint sources:
taint:
mode: audit
inherit_sensitive_paths: true
Inherited sensitive paths use the secret label when their effective
sensitive-path policy is ask or block. If you desensitize a path so it no
longer has an ask/block policy, it is not inherited as a taint source.
Add explicit sources with your own labels:
taint:
mode: audit
sources:
- paths:
- ".env*"
- "config/prod/**"
labels: [secret, prod_config]
- paths:
- "customers/**/*.csv"
labels: [customer_data]
Source patterns are matched against the raw path, resolved path, friendly path, and basename.
Propagation¶
Propagation means a tainted session wrote data into another target. nah then treats that target as tainted for later actions.
Supported v1 propagation targets:
| Action type | Default | What becomes tainted |
|---|---|---|
filesystem_write |
on | The written file path |
git_write |
on | The current repository |
browser_file |
on | The browser file target |
Configure propagation with booleans:
taint:
mode: audit
propagation:
filesystem_write: true
git_write: true
browser_file: true
Custom action types cannot be propagation targets in v1. They can be marked as activation or boundary sinks.
Activation and Boundary Sinks¶
Built-in activation sinks include code and package execution, container execution, browser execution, and agent execution actions.
Built-in boundary sinks include network access, remote git writes, database writes, service actions, browser state/file actions, and remote agent actions.
Add or remove action types from the sink categories:
taint:
mode: audit
categories:
activation:
add: [mytool_run]
remove: []
boundary:
add: [mytool_upload]
remove: []
Use this for custom action types that you classify with nah classify or
classify: config. Do not use it for unknown command names; unknown actions
remain a separate taint category and default to ask.
Policies¶
Policies are per label. A policy key can be a specific action type, or one of
the category keys activation, boundary, or unknown.
taint:
mode: enforce
policies:
default:
activation: audit
boundary: ask
unknown: ask
secret:
activation: audit
boundary: ask
git_remote_write: block
customer_data:
activation: ask
boundary: block
unknown: ask
Valid policies are allow, audit, ask, and block.
Specific action-type policies win before category policies. In the example
above, secret + git_remote_write = block is stricter than
secret + boundary = ask, so a remote git write after secret access blocks.
In v1, unknown cannot be loosened below ask.
Project Config Trust¶
Global config can set the full taint shape. Project .nah.yaml files are more
restricted:
taint.modeandtaint.inherit_sensitive_pathsare global-only.- Trusted projects can add sources, categories, propagation settings, and policies.
- Untrusted projects can only tighten policies.
Trust a project root only when you want that project to define richer taint metadata for itself:
nah trust-project
Local State¶
Taint state is local session state under:
~/.config/nah/taint/sessions/
State contains labels, compact target identities, and chain metadata. It does not store command output or file contents.
Limitations¶
- Taint tracking is action/category based, not byte-level dataflow analysis.
- It does not inspect process memory.
- Generic build commands can activate code after taint exists; use audit mode before enforcing strict activation policies.
- Codex enforcement depends on the local interactive hook surface. If Codex
runs a tool without a
PermissionRequest, PreToolUse/PostToolUse can still record audit metadata but cannot force Codex's native approval UI. - Terminal Guard taint is audit-only in v1.