Harness / Tutorials
Intermediate 20 min

Policy-as-Code

Govern exactly what your AI agent can do using YAML rules — version-controlled, auditable, and enforced at runtime.

Intermediate 20 min read

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.

⚡ Unique to Harness
No other coding agent has a policy engine. Claude Code has basic permission modes — 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.
ℹ What Policies Control
Policies govern tool calls: 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".

YAML .harness/policy.yml
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:

Bash
harness --permission default "Clean up the project"
# The agent can read files freely, but will be blocked from rm -rf and .env writes
✓ Rules are evaluated in order
The first matching rule wins. If no rule matches a tool call, the 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.

YAML
# 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.

YAML
# 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.

YAML
# 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.

YAML
# 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
~/.harness/policy.yml
Global rules for all projects on this machine
Team Level
~/team/.harness/policy.yml
Shared across projects in a team repo; inherits org rules
Project Level
.harness/policy.yml
Project-specific rules; inherits team rules

Org-Level Policy

YAML ~/.harness/policy.yml
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

YAML ~/team/.harness/policy.yml
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

YAML .harness/policy.yml
version: 1
inherit_from: "../team-policy.yml"  # Pull in team rules

rules:
  - tool: Read
    decision: allow
    description: "Project: Allow all reads without prompting"
ℹ Evaluation Order
Harness evaluates rules from the most specific (project) to least specific (org). The first matching rule wins across the entire chain. This means project rules can override org rules — so be intentional about what you allow at the project level.

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.

Python simulate_policy.py
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:

TOML .harness/config.toml
[policy]
policy_paths = [".harness/policy.yml"]
simulation_mode = true  # Log decisions but don't enforce them
▶ Try It
Run simulation mode first to see what your policy would block. Check the audit log for unexpected denies (false positives) and unexpected allows (gaps). Only set simulation_mode = false when the log looks correct.
Python harness/permissions/policy.py
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.

YAML .harness/policy.yml
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"
⚠ Pattern Syntax by Condition Type
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.
Python harness/permissions/policy.py
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

Python policy_check.py
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

Python
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:

Python run_with_policy.py
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())
ℹ Policy Config in TOML
You can override which policy files are loaded and enable simulation mode via .harness/config.toml:
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.

Python tests/test_policy.py
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:

  1. Start with the production safety policy from Section 6 as your baseline
  2. Run with simulation_mode = true for one day — review the audit log for false positives
  3. Add project-specific rules for your directory structure
  4. Commit .harness/policy.yml to version control and require reviews on changes to it
  5. Add pytest tests (see Deep Dive in Section 7) to catch policy regressions in CI
▶ Try It Now
Create .harness/policy.yml with the production safety policy, then run:
Bash
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.