AID-H-021.001: Client-Side Configuration Enforcement

Version 1.20261015 · Actionable sub-technique

Proactively prevents the creation and use of insecure AI agent configurations on developer endpoints. This is a 'shift-left' defense that uses endpoint security tools, policy-as-code scanners, and local development guardrails to block high-risk settings before they can ever be committed to source control or executed.

Pillars: infra, app

Lifecycle: building

Scope boundary

Owns developer-endpoint, pre-commit, repository, schema, and policy controls that prevent insecure agent configuration from being created, committed, or accepted during development.

AID-H-021.002: AID-H-021.001 blocks insecure configuration before commit or build acceptance; AID-H-021.002 verifies and enforces signed configuration at agent startup and runtime. Shift-left checks do not prove the running agent loaded the approved bytes.

AID-H-021.005: AID-H-021.001 prevents unsafe workspace and agent configuration from being authored or accepted in source; AID-H-021.005 evaluates the effective workspace manifest before execution-capable surfaces activate. A pre-commit check does not grant runtime workspace trust, and the trust gate does not replace source-policy enforcement.

Implementation guidance

AID-H-021.001-G001

Use EDR/XDR to block dangerous agent configurations at the endpoint.
Concept:

Use your existing Endpoint Detection and Response (EDR) platform to create custom rules that monitor for file content indicative of a high-risk agent configuration. This provides a centrally managed, difficult-to-bypass control on all developer machines.

Implement a Custom EDR Rule

The endpoint policy scans organization-owned agent configuration paths for reviewed high-risk combinations and sends matches to quarantine or review. Prevention capability and event schema are product-specific; the portable detection artifact below is a valid YARA rule, not a fictitious cross-vendor EDR language.

# File: endpoint_rules/agent_config_high_risk.yar
rule Agent_Config_High_Risk
{
  meta:
    owner = "ai-security"
    purpose = "Detect high-risk agent configuration language for review"
  strings:
    $disable_1 = "disable confirmations" ascii wide nocase
    $disable_2 = "bypass safety measures" ascii wide nocase
    $privilege = "auto-privilege-escalation" ascii wide nocase
    $execution = "immediate execution" ascii wide nocase
  condition:
    filesize < 1MB and 2 of them
}

Deploy the compiled YARA rule through an endpoint product that supports content scanning on create/modify events, and scope it to the organization-owned agent policy/config paths. Map a match to quarantine or review according to endpoint-product capability; do not label generic YARA itself as a write-blocking engine. Keep a signed allowlist for approved test fixtures and security documentation so keyword examples do not create fleet-wide false positives.

Verification and evidence

Test blocked or quarantined malicious fixtures, benign policy files, documentation containing one quoted phrase, renamed files, UTF-16 content, and an unavailable endpoint sensor. Preserve rule digest/version, endpoint policy readback, sensor health, file digest/path, matched strings, disposition, allowlist decision, and explicit PASS/FAIL/INSUFFICIENT_DATA/ERROR verdict.

Action: Deploy a custom EDR rule that monitors for and blocks file modifications containing high-risk keywords in known AI agent configuration files across your developer fleet.

AID-H-021.001-G002

Implement a Git pre-commit hook to scan for insecure settings before they enter the repository.
Concept

This hook is a developer fast-feedback control. It scans the exact staged blobs, not mutable worktree files, and central CI remains the authoritative merge gate because local hooks can be bypassed.

Install a staged-blob pre-commit hook
#!/usr/bin/env python3
# File: .git/hooks/pre-commit
from __future__ import annotations

import math
import os
import re
import subprocess
import sys

TARGET = re.compile(r"(?:^|/)(?:CLAUDE|agent_config)\.md$", re.IGNORECASE)
PATTERNS = (
    re.compile(r"disable\s+confirmations", re.IGNORECASE),
    re.compile(r"immediate\s+execution", re.IGNORECASE),
)
def positive_int(name: str) -> int:
    value = int(os.environ[name])
    if value <= 0:
        raise RuntimeError(f"{name} must be positive")
    return value


def positive_seconds(name: str) -> float:
    value = float(os.environ[name])
    if not math.isfinite(value) or value <= 0:
        raise RuntimeError(f"{name} must be finite and positive")
    return value


MAX_CONFIG_BYTES = positive_int("VERIFIED_H021_STAGED_CONFIG_MAX_BYTES")
MAX_NAME_LIST_BYTES = positive_int("VERIFIED_H021_STAGED_NAME_LIST_MAX_BYTES")
GIT_TIMEOUT_SECONDS = positive_seconds("VERIFIED_H021_GIT_TIMEOUT_SECONDS")

raw = subprocess.run(
    ["git", "diff", "--cached", "--name-only", "-z", "--diff-filter=ACMR"],
    check=True, capture_output=True, timeout=GIT_TIMEOUT_SECONDS,
).stdout
if len(raw) > MAX_NAME_LIST_BYTES:
    raise SystemExit("[agent-config] ERROR staged path list exceeds signed limit")
paths = [item.decode("utf-8", "strict") for item in raw.split(b"\0") if item]
findings: list[dict] = []
for path in paths:
    if not TARGET.search(path):
        continue
    blob_size_raw = subprocess.run(
        ["git", "cat-file", "-s", f":{path}"], check=True, capture_output=True,
        timeout=GIT_TIMEOUT_SECONDS,
    ).stdout
    try:
        blob_size = int(blob_size_raw.decode("ascii", "strict").strip())
    except (UnicodeDecodeError, ValueError):
        findings.append({"path": path, "reason": "staged_blob_size_invalid"})
        continue
    if blob_size > MAX_CONFIG_BYTES:
        findings.append({"path": path, "reason": "staged_config_exceeds_policy_limit"})
        continue
    blob = subprocess.run(
        ["git", "show", f":{path}"], check=True, capture_output=True,
        timeout=GIT_TIMEOUT_SECONDS,
    ).stdout
    if len(blob) != blob_size or len(blob) > MAX_CONFIG_BYTES:
        findings.append({"path": path, "reason": "staged_config_exceeds_policy_limit"})
        continue
    try:
        text = blob.decode("utf-8", "strict")
    except UnicodeDecodeError:
        findings.append({"path": path, "reason": "staged_config_is_not_utf8"})
        continue
    for pattern in PATTERNS:
        if pattern.search(text):
            findings.append({"path": path, "reason": pattern.pattern})

if findings:
    for finding in findings:
        print(f"[agent-config] FAIL {finding}", file=sys.stderr)
    raise SystemExit(1)
print("[agent-config] PASS staged dangerous-phrase fast check")
Verification and evidence

Test staged-vs-worktree divergence, filenames containing spaces, renamed files, non-UTF-8 data, an oversized file, one forbidden phrase, and benign documentation. Keep the hook version/digest and CI replay result. A local pass is not a control pass until the required CI schema/policy job independently evaluates the same commit tree.

Action: Distribute the hook with the pre-commit framework, but require the H-021.001 schema/policy job on the protected branch.

AID-H-021.001-G003

Validate agent configurations against a strict schema and policy set before commit or build acceptance.
Concept

Validate every changed configuration in the build path with both Draft 2020-12 JSON Schema and a Conftest policy that is explicitly evaluated in the intended namespace. The schema loader rejects duplicate keys, multiple YAML documents, non-finite numbers, YAML-only value types, symlinks, out-of-tree paths, and an empty file list so malformed or skipped input cannot become a pass.

Define the schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "required": ["execution_mode", "security"],
  "additionalProperties": false,
  "properties": {
    "execution_mode": {
      "type": "string",
      "enum": ["confirmed", "review_required"]
    },
    "security": {
      "type": "object",
      "required": ["sandboxing_enabled", "policy_bundle_version"],
      "additionalProperties": false,
      "properties": {
        "sandboxing_enabled": {"type": "boolean", "const": true},
        "policy_bundle_version": {"type": "string", "pattern": "^v\\S+$"}
      }
    }
  }
}
Use the Conftest default namespace deliberately
# File: policy/agent-config/agent_config.rego
package main
import rego.v1

deny contains msg if {
  input.execution_mode == "immediate_unconfirmed"
  msg := "execution_mode must require confirmation or review"
}

deny contains msg if {
  not input.security.sandboxing_enabled
  msg := "security.sandboxing_enabled must be true"
}

deny contains msg if {
  not startswith(input.security.policy_bundle_version, "v")
  msg := "policy bundle version must be explicit and versioned"
}
Pass only pre-commit's matched filenames
# File: .pre-commit-config.yaml
repos:
  - repo: local
    hooks:
      - id: agent-config-schema
        name: agent-config-schema
        entry: python scripts/validate_agent_config.py
        language: system
        pass_filenames: true
        files: ^configs/.*\.(json|ya?ml)$
      - id: agent-config-policy
        name: agent-config-policy
        entry: conftest test --policy policy/agent-config --namespace main
        language: system
        pass_filenames: true
        files: ^configs/.*\.(json|ya?ml)$
Parse strictly and check the schema once
# File: scripts/validate_agent_config.py
from __future__ import annotations

import json
import math
import sys
from pathlib import Path
from typing import NoReturn

import jsonschema
import yaml
from yaml.nodes import MappingNode
from yaml.resolver import BaseResolver

ROOT = Path(__file__).resolve().parents[1]
CONFIG_ROOT = (ROOT / "configs").resolve(strict=True)
SCHEMA_PATH = ROOT / "schemas" / "agent-config.schema.json"


def fail(message: str) -> NoReturn:
    raise ValueError(message)


def strict_json(text: str, label: str) -> object:
    def pairs(items):
        value = {}
        for key, item in items:
            if key in value:
                fail(f"{label}: duplicate key {key!r}")
            value[key] = item
        return value

    def constant(value):
        fail(f"{label}: non-finite number {value}")

    return json.loads(text, object_pairs_hook=pairs, parse_constant=constant)


class StrictLoader(yaml.SafeLoader):
    pass


def strict_mapping(loader: StrictLoader, node: MappingNode, deep: bool = False) -> dict:
    if not isinstance(node, MappingNode):
        fail("YAML mapping node required")
    value = {}
    for key_node, value_node in node.value:
        if key_node.tag == "tag:yaml.org,2002:merge":
            fail("YAML merge keys are not allowed")
        key = loader.construct_object(key_node, deep=deep)
        if type(key) is not str:
            fail("all object keys must be strings")
        if key in value:
            fail(f"duplicate YAML key {key!r}")
        value[key] = loader.construct_object(value_node, deep=deep)
    return value


StrictLoader.add_constructor(BaseResolver.DEFAULT_MAPPING_TAG, strict_mapping)


def require_json_value(value: object, location: str = "$") -> None:
    if value is None or type(value) in (str, bool, int):
        return
    if type(value) is float:
        if not math.isfinite(value):
            fail(f"{location}: non-finite number")
        return
    if type(value) is list:
        for index, item in enumerate(value):
            require_json_value(item, f"{location}[{index}]")
        return
    if type(value) is dict:
        for key, item in value.items():
            if type(key) is not str:
                fail(f"{location}: non-string key")
            require_json_value(item, f"{location}.{key}")
        return
    fail(f"{location}: value is not JSON-compatible")


def parse_config(path: Path, text: str) -> object:
    if path.suffix == ".json":
        value = strict_json(text, str(path))
    elif path.suffix in (".yaml", ".yml"):
        documents = list(yaml.load_all(text, Loader=StrictLoader))
        if len(documents) != 1:
            fail(f"{path}: exactly one YAML document is required")
        value = documents[0]
    else:
        fail(f"{path}: unsupported extension")
    require_json_value(value)
    return value


SCHEMA = strict_json(SCHEMA_PATH.read_text(encoding="utf-8", errors="strict"), str(SCHEMA_PATH))
jsonschema.Draft202012Validator.check_schema(SCHEMA)
VALIDATOR = jsonschema.Draft202012Validator(
    SCHEMA, format_checker=jsonschema.FormatChecker()
)


def validate_file(argument: str) -> None:
    supplied = Path(argument)
    if supplied.is_symlink():
        fail(f"{argument}: symbolic links are not allowed")
    resolved = supplied.resolve(strict=True)
    resolved.relative_to(CONFIG_ROOT)
    if not resolved.is_file():
        fail(f"{argument}: regular file required")
    value = parse_config(
        resolved, resolved.read_text(encoding="utf-8", errors="strict")
    )
    errors = sorted(
        VALIDATOR.iter_errors(value),
        key=lambda item: tuple(str(part) for part in item.absolute_path),
    )
    if errors:
        fail("; ".join(error.message for error in errors))


def main(arguments: list[str]) -> int:
    if not arguments:
        print("[agent-config] no input files; refusing an empty validation", file=sys.stderr)
        return 2
    failed = False
    for argument in arguments:
        try:
            validate_file(argument)
        except Exception as exc:
            failed = True
            print(f"[agent-config] {argument}: {type(exc).__name__}: {exc}", file=sys.stderr)
    return 1 if failed else 0


if __name__ == "__main__":
    raise SystemExit(main(sys.argv[1:]))
Exercise positive and negative paths in CI
# Local change gate and full CI gate
pre-commit run agent-config-schema --all-files
pre-commit run agent-config-policy --all-files

# Required negative fixtures: each command must fail.
if python scripts/validate_agent_config.py tests/fixtures/config-duplicate-key.yaml; then
  exit 1
fi
if conftest test --policy policy/agent-config --namespace main \
  tests/fixtures/config-immediate-unconfirmed.yaml; then
  exit 1
fi

Action: Keep the Rego package and the explicit --namespace main selection aligned, isolate this policy in its own directory, and run both hooks over all tracked configuration files in CI. Any parser, schema, policy, dependency, or empty-input error blocks commit and build acceptance.

AID-H-021.001-G004

Protect production configuration repositories with signed commits, branch protection, and required code-owner approval.
Concept

Configuration integrity starts before runtime. If the repository that stores production agent configs allows force-pushes, unsigned commits, or unreviewed merges, later signature checks only verify that the wrong file was faithfully shipped. Treat production configuration repositories as security-sensitive control planes.

Step 1: Require CODEOWNERS review for production config paths
# File: CODEOWNERS
/configs/production/** @ai-security-team @ml-platform-team
/policies/agent/** @ai-security-team
Step 2: Enforce signed commits and linear reviewed merges on the protected branch
{
  "required_status_checks": {
    "strict": true,
    "contexts": ["agent-config-schema", "agent-config-policy"]
  },
  "enforce_admins": true,
  "required_pull_request_reviews": {
    "dismiss_stale_reviews": true,
    "require_code_owner_reviews": true,
    "required_approving_review_count": 2,
    "require_last_push_approval": true
  },
  "restrictions": null,
  "required_linear_history": true,
  "allow_force_pushes": false,
  "allow_deletions": false,
  "required_conversation_resolution": true
}
# Save the JSON above as branch-protection.json.
: "${GITHUB_REPOSITORY:?set owner/repository}"
BRANCH="${PROTECTED_BRANCH:-main}"
API_VERSION="2026-03-10"
mkdir -p artifacts

gh api --method PUT   -H "X-GitHub-Api-Version: $API_VERSION"   "repos/$GITHUB_REPOSITORY/branches/$BRANCH/protection"   --input branch-protection.json
gh api --method POST   -H "X-GitHub-Api-Version: $API_VERSION"   "repos/$GITHUB_REPOSITORY/branches/$BRANCH/protection/required_signatures"

# Independent readback: retain both responses as the control evidence.
gh api -H "X-GitHub-Api-Version: $API_VERSION"   "repos/$GITHUB_REPOSITORY/branches/$BRANCH/protection"   > artifacts/branch-protection-readback.json
gh api -H "X-GitHub-Api-Version: $API_VERSION"   "repos/$GITHUB_REPOSITORY/branches/$BRANCH/protection/required_signatures"   > artifacts/required-signatures-readback.json
Step 3: Block direct pushes and require PR-based evidence

Require pull requests to include the config diff, the schema/policy validation status, and the incident or change ticket that justifies the update. Keep the merge commit, approver identities, and commit signature verification result as evidence for later audit.

Action: Put production config paths behind branch protection, CODEOWNERS review, and signed-commit enforcement. This gives the runtime verifier a trustworthy upstream source of truth instead of a mutable repository history.

Open-source tools

Commercial / hosted tools

Threat mappings

MITRE ATLAS

MAESTRO

OWASP LLM Top 10 2026

OWASP ML Top 10 2023

OWASP Top 10 for Agentic Applications 2026

NIST Adversarial Machine Learning 2025

Cisco Integrated AI Security and Safety Framework

Google Secure AI Framework 2.0 - Risks

Databricks AI Security Framework 3.0

AID-H-021.001: Client-Side Configuration Enforcement | AIDEFEND

Loading interactive view…

A Practical Knowledge Base for AI Security Defenses

Version