Policy-as-Code
Govern exactly what your AI agent can do using YAML rules — version-controlled, auditable, and enforced at runtime.
1Why Policy-as-Code?
Compliance frameworks like SOC 2, HIPAA, and PCI-DSS require documented access controls. When your auditor asks "How do you control what AI agents can do in production?" — with Harness, you hand them a YAML file.
Policy-as-Code means your agent's permission rules live in source control alongside your application code.
Every change is tracked, reviewed in pull requests, and rolled back with a single git revert.
default,
auto-edit, bypass — but no rule matching. Cursor has no programmatic controls at
all. Harness is the only agent where access control is declarative, auditable, and composable.
Read, Write, Edit, Bash,
Glob, Grep, and any custom tools you register. Each rule can allow,
deny, or ask (prompt the user at runtime) based on pattern-matched conditions.
2Your First Policy File
Create a file at .harness/policy.yml in your project root. Harness loads this automatically
on every run when permission_mode is "default" or "policy".
version: 1
defaults:
decision: ask # Default: ask for permission on any unmatched tool
rules:
- tool: Read
decision: allow
description: "Allow reading any file"
- tool: Glob
decision: allow
description: "Allow file searching"
- tool: Grep
decision: allow
description: "Allow content searching"
- tool: Bash
decision: deny
when:
command_matches: "rm -rf*"
description: "Block recursive deletion"
- tool: Write
decision: deny
when:
path_matches: "*.env"
description: "Protect environment files"
Run the agent with your policy active:
harness --permission default "Clean up the project"
# The agent can read files freely, but will be blocked from rm -rf and .env writes
defaults.decision applies.
Put more specific rules before more general ones.
3Condition Types
Each rule can have zero or more conditions. A rule without conditions matches all calls to that tool. When conditions are present, all conditions must match for the rule to apply.
| Condition | Field | Description | Example Pattern |
|---|---|---|---|
command_matches |
pattern |
fnmatch glob match on Bash command string | rm -rf* |
path_matches |
pattern |
fnmatch glob match on file path argument | *.env |
not_path_matches |
pattern |
Inverse — denies if path does NOT match (fnmatch glob) | src/*.py |
content_matches |
pattern |
Regex match on content being written (re.search) | password |
Match the shell command string passed to the Bash tool using an fnmatch glob pattern. Use one rule per pattern — fnmatch does not support | alternation.
# Block network exfiltration tools (one rule per command)
- tool: Bash
decision: deny
when:
command_matches: "curl*"
description: "Block curl"
- tool: Bash
decision: deny
when:
command_matches: "wget*"
description: "Block wget"
- tool: Bash
decision: deny
when:
command_matches: "nc *"
description: "Block netcat"
Match the file path argument to Write, Edit, or Read using an fnmatch glob pattern. Use one rule per extension — fnmatch does not support | alternation.
# Protect secrets and certificate files (one rule per extension)
- tool: Write
decision: deny
when:
path_matches: "*.env"
description: "Protect .env files"
- tool: Write
decision: deny
when:
path_matches: "*.pem"
description: "Protect PEM certificates"
- tool: Write
decision: deny
when:
path_matches: "*.key"
description: "Protect private keys"
- tool: Write
decision: deny
when:
path_matches: "*.crt"
description: "Protect certificate files"
- tool: Write
decision: deny
when:
path_matches: "*.p12"
description: "Protect PKCS12 keystores"
Deny when the path does NOT match. Useful for restricting writes to a specific directory. Uses fnmatch glob pattern matching.
# Only allow writes inside src/ — deny everything else
- tool: Write
decision: deny
when:
not_path_matches: "src/*"
description: "Only allow writes to src/"
Inspect the actual file content the agent is trying to write. Prevents leaking secrets into source files. content_matches uses re.search() (Python regex), so regex patterns apply here.
# Block writing secrets to any file
- tool: Write
decision: deny
when:
content_matches: "(api_key|secret|password)\\s*="
description: "Block writing secrets to files"
4Policy Inheritance
Policies can inherit from a parent policy using inherit_from. Child rules are evaluated
first; unmatched calls fall through to the parent. This lets you build a hierarchy:
org-level baseline → team customization → project specifics.
Org-Level Policy
version: 1
defaults:
decision: ask
rules:
- tool: Bash
decision: deny
when:
command_matches: "rm -rf /*"
description: "Org: Never rm -rf root paths"
Team-Level Policy
version: 1
inherit_from: "~/.harness/policy.yml" # Pull in org rules first
rules:
- tool: Write
decision: deny
when:
path_matches: "*.prod.*"
description: "Team: Protect production configs"
Project-Level Policy
version: 1
inherit_from: "../team-policy.yml" # Pull in team rules
rules:
- tool: Read
decision: allow
description: "Project: Allow all reads without prompting"
5Simulation Mode
Before enforcing a new policy, run it in simulation mode. Harness will log every decision it would have made — without actually blocking anything. Fix false positives before going live.
from harness.permissions.policy import PolicyEngine
engine = PolicyEngine(simulation_mode=True)
engine.load_file(".harness/policy.yml")
# Test what would happen — no actual enforcement
results = engine.simulate("Bash", {"command": "rm -rf /tmp/test"})
for r in results:
print(f"Rule: {r['description']}")
print(f"Decision: {r['decision']}")
print(f"Conditions: {r['conditions']}")
print("---")
Enable simulation mode in TOML to use it with the CLI:
[policy]
policy_paths = [".harness/policy.yml"]
simulation_mode = true # Log decisions but don't enforce them
simulation_mode = false when the
log looks correct.
from pathlib import Path
from typing import Any
from harness.permissions.policy import PolicyEngine, Policy, PolicyRule
from harness.permissions.rules import PermissionDecision
class PolicyEngine:
def __init__(self, *, simulation_mode: bool = False): ...
@property
def policies(self) -> list[Policy]:
"""All loaded Policy objects, in evaluation order."""
...
@property
def audit_log(self) -> list[dict[str, Any]]:
"""Every tool check recorded since engine was created."""
...
def load_file(self, path: str | Path) -> None:
"""Load a single YAML policy file."""
...
def load_files(self, paths: list[str | Path]) -> None:
"""Load multiple YAML policy files at once."""
...
def check(
self,
tool_name: str,
args: dict | None = None,
) -> PermissionDecision | None:
"""
Evaluate all loaded rules for this tool call.
Returns the winning decision, or None if no rule matched
(caller should apply defaults).
In simulation_mode, always returns None but logs the decision.
"""
...
def simulate(
self,
tool_name: str,
args: dict | None = None,
) -> list[dict]:
"""
Run the policy engine against a hypothetical tool call.
Returns a list of rule evaluation results — use for testing.
Does not modify the audit_log.
"""
...
6Build a Production Safety Policy
Here is a complete policy for production environments. Copy this as a starting point and tune the patterns to your directory structure.
version: 1
defaults:
decision: ask
rules:
# === ALLOW: Safe read operations ===
- tool: Read
decision: allow
description: "Allow reading source files"
- tool: Glob
decision: allow
description: "Allow file discovery"
- tool: Grep
decision: allow
description: "Allow code search"
# === DENY: Dangerous shell operations ===
- tool: Bash
decision: deny
when:
command_matches: "rm -rf*"
description: "Block recursive deletion"
- tool: Bash
decision: deny
when:
command_matches: "curl*"
description: "Block curl (network exfiltration)"
- tool: Bash
decision: deny
when:
command_matches: "wget*"
description: "Block wget (network exfiltration)"
- tool: Bash
decision: deny
when:
command_matches: "nc *"
description: "Block netcat"
- tool: Bash
decision: deny
when:
command_matches: "ncat*"
description: "Block ncat"
- tool: Bash
decision: deny
when:
command_matches: "chmod 777*"
description: "Block world-writable permission changes"
- tool: Bash
decision: deny
when:
command_matches: "chmod +x*"
description: "Block making files executable"
# === DENY: Secrets and certificates ===
- tool: Write
decision: deny
when:
path_matches: "*.env"
description: "Protect .env files"
- tool: Write
decision: deny
when:
path_matches: "*.pem"
description: "Protect PEM certificates"
- tool: Write
decision: deny
when:
path_matches: "*.key"
description: "Protect private keys"
- tool: Write
decision: deny
when:
path_matches: "*.crt"
description: "Protect certificate files"
- tool: Write
decision: deny
when:
path_matches: "*.p12"
description: "Protect PKCS12 keystores"
- tool: Edit
decision: deny
when:
path_matches: "*.env"
description: "Protect .env files"
- tool: Edit
decision: deny
when:
path_matches: "*.pem"
description: "Protect PEM certificates"
- tool: Edit
decision: deny
when:
path_matches: "*.key"
description: "Protect private keys"
- tool: Edit
decision: deny
when:
path_matches: "*.crt"
description: "Protect certificate files"
- tool: Edit
decision: deny
when:
path_matches: "*.p12"
description: "Protect PKCS12 keystores"
- tool: Write
decision: deny
when:
content_matches: "(BEGIN (RSA |EC )?PRIVATE KEY|password\\s*=|api_key\\s*=)"
description: "Block writing secrets to any file"
command_matches, path_matches, and not_path_matches use
fnmatch glob patterns — use * for wildcards, ? for a single
character. content_matches is the only condition that uses Python regex
(re.search). In YAML double-quoted strings, regex backslashes must be escaped:
\\s for whitespace, \\. for a literal dot.
from dataclasses import dataclass, field
from harness.permissions.rules import PermissionDecision
# harness/permissions/policy.py
@dataclass(frozen=True)
class PolicyRule:
tool: str
decision: PermissionDecision # PermissionDecision.ALLOW | .DENY | .ASK
when: dict[str, str] = field(default_factory=dict) # condition_type -> pattern
description: str = ""
@dataclass(frozen=True)
class Policy:
version: int = 1
rules: tuple[PolicyRule, ...] = ()
defaults: dict[str, str] = field(default_factory=dict)
inherit_from: str | None = None # Path to parent policy file
# harness/types/config.py
@dataclass
class PolicyConfig:
policy_paths: tuple[str, ...] = ()
simulation_mode: bool = False
7PolicyEngine SDK Usage
You can use the policy engine directly in Python for custom workflows, CI enforcement, or policy testing.
Basic Check and Audit Log
from harness.permissions.policy import PolicyEngine
from harness.permissions.rules import PermissionDecision
# Load and check
engine = PolicyEngine()
engine.load_file(".harness/policy.yml")
# Check a tool call
decision = engine.check("Bash", {"command": "rm -rf /tmp"})
if decision == PermissionDecision.DENY:
print("Blocked by policy!")
elif decision == PermissionDecision.ALLOW:
print("Permitted.")
elif decision is None:
print("No rule matched — applying defaults.")
# View the complete audit log
for entry in engine.audit_log:
tool = entry["tool"]
decision = entry["decision"]
rule = entry.get("description", "default")
print(f"{tool:10} -> {decision:6} [{rule}]")
Loading Multiple Policy Files
from harness.permissions.policy import PolicyEngine
engine = PolicyEngine()
# Load org + team + project in one call (evaluated in list order)
engine.load_files([
"~/.harness/policy.yml",
"~/team/.harness/policy.yml",
".harness/policy.yml",
])
Integration with harness.run()
When using the CLI with --permission default, Harness automatically loads
.harness/policy.yml and ~/.harness/policy.yml. In Python, the same applies:
import asyncio
import harness
async def main():
# Policy is automatically loaded from:
# 1. ~/.harness/policy.yml (org/user level)
# 2. .harness/policy.yml (project level)
# when permission_mode is "default"
async for msg in harness.run(
"Deploy to production",
permission_mode="default",
):
print(msg)
asyncio.run(main())
.harness/config.toml:
[policy]
policy_paths = [
"~/.harness/policy.yml",
".harness/policy.yml",
]
simulation_mode = false # Set to true to log without enforcing
You can write pytest tests to assert that your policy allows and blocks the right operations. Run these in CI to catch policy regressions before they reach production.
import pytest
from harness.permissions.policy import PolicyEngine
from harness.permissions.rules import PermissionDecision
@pytest.fixture
def engine():
e = PolicyEngine()
e.load_file(".harness/policy.yml")
return e
def test_read_is_allowed(engine):
decision = engine.check("Read", {"path": "src/main.py"})
assert decision == PermissionDecision.ALLOW
def test_rm_rf_is_denied(engine):
decision = engine.check("Bash", {"command": "rm -rf /"})
assert decision == PermissionDecision.DENY
def test_env_write_is_denied(engine):
decision = engine.check("Write", {"path": ".env"})
assert decision == PermissionDecision.DENY
def test_src_write_is_asked(engine):
# No rule matches — should use default (ask)
decision = engine.check("Write", {"path": "src/utils.py"})
assert decision is None # No rule matched; caller applies default
8Next Steps
You now have declarative, auditable control over your agent's behavior. Here is how to roll this out:
- Start with the production safety policy from Section 6 as your baseline
- Run with
simulation_mode = truefor one day — review the audit log for false positives - Add project-specific rules for your directory structure
- Commit
.harness/policy.ymlto version control and require reviews on changes to it - Add pytest tests (see Deep Dive in Section 7) to catch policy regressions in CI
.harness/policy.yml with the production safety policy, then run:
harness --permission default "List all Python files and summarize what each does"
# The agent will read and glob freely, but ask before any write or bash command
The next tutorial covers Audit Logging — how to export, query, and ship Harness audit events to your SIEM or compliance dashboard.