AID-H-021.001: Client-Side Configuration Enforcement
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.
- AID-H-021.001-G002: Implement a Git pre-commit hook to scan for insecure settings before they enter the repository.
- AID-H-021.001-G003: Validate agent configurations against a strict schema and policy set before commit or build acceptance.
- AID-H-021.001-G004: Protect production configuration repositories with signed commits, branch protection, and required code-owner approval.
AID-H-021.001-G001
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
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
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
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
- Git
- osquery (endpoint telemetry queries for configuration drift detection)
- Code - OSS (MIT-licensed Extension API implementation)
- Semgrep
- Open Policy Agent (OPA)
- Conftest
- pre-commit
- Checkov
Commercial / hosted tools
- CrowdStrike Falcon Insight XDR
- SentinelOne Singularity Endpoint
- Microsoft Defender for Endpoint
- Jamf Pro
- Microsoft Intune
Threat mappings
MITRE ATLAS
- AML.T0081 Modify AI Agent Configuration
- AML.T0095.000 Search Open Websites/Domains: Code Repositories (pre-commit config checks prevent agent config exposure in repos)
MAESTRO
- Agent Goal Manipulation (L7)
OWASP LLM Top 10 2026
- LLM03:2026 Excessive Agency (endpoint policy blocks high-risk agent settings before use, while runtime action authorization remains separately enforced)
OWASP ML Top 10 2023
- N/A
OWASP Top 10 for Agentic Applications 2026
- N/A
NIST Adversarial Machine Learning 2025
- N/A
Cisco Integrated AI Security and Safety Framework
- AISubtech-2.1.1 Execution Approval Bypass
- AISubtech-5.2.1 Agent Profile Tampering
- AITech-2.1 Excessive Agency
- AITech-5.2 Configuration Persistence
Google Secure AI Framework 2.0 - Risks
- N/A
Databricks AI Security Framework 3.0
- Agents - Core 13.6: Intent Breaking & Goal Manipulation
- Agents - Tools MCP Server 13.20: Insecure Server Configuration
- Model 7.4: Source code control attack