Policy-as-Code — Harness Tutorials

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