CtrlK
BlogDocsLog inGet started
Tessl Logo
SkillPublic

ai-governance

Generate and enforce policy gates for AI coding agents (Copilot, Claude Code) — real-time session hooks that deny protected-path edits and dangerous commands, plus a merge-time backstop for anything that bypasses them. Use when asked to "govern AI agents", "block AI from touching secrets", "add an AI policy gate", or "why did the AI agent hook not fire".

Quality

86%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide
SecuritybySnyk

Passed

No findings from the security scan

SKILL.md
Quality
Evals
Security

Generate and enforce a policy gate for AI coding agents operating on this repo: real-time session hooks for Copilot and Claude Code, plus a merge-time GitHub Actions check that catches anything the hooks miss.

Read references/ai-governance.md before responding.


Interactive Wizard (fires when no arguments are provided)

When invoked with no arguments, ask before proceeding:

Q1 — Mode?

What do you need?
  1. generate — scaffold policy, evaluator, hooks, and merge-time check
  2. check    — dry-run the evaluator against a path, command, or diff
  3. audit    — scan repos in an org for policy presence, tier, and drift
  4. explain  — plain-English translation of an existing .ai-governance.yaml

Enter 1-4 or mode name:

Q2 — Context (after mode selected, one at a time):

  • generate: Which default policy pack? (terraform / kubernetes / generic / blank)
  • check: Give me a file path, a command string, or a diff range (e.g. main...HEAD) to test:
  • audit: Which org or repo list should I scan?
  • explain: Path to the .ai-governance.yaml to explain (default: ./.ai-governance.yaml):

Then proceed into the relevant mode below.


Mode: generate

Scaffold the policy file, evaluator, session hooks, and merge-time check for this repo.

Steps:

  1. Detect which AI tools are configured (.github/copilot/, .claude/, .cursor/, .codex/ if present) and which CI system is in use — reuse the scan approach setup-agents.md already implements; do not reimplement it.

  2. Write .ai-governance.yaml with the chosen default pack. All packs share this shape; only protected_paths differs:

    generic:

    version: 1
    source: local
    enforcement: audit
    protected_paths:
      - ".github/workflows/**"
      - ".github/hooks/**"
      - ".claude/settings.json"
      - ".ai-governance.yaml"
      - ".ai-governance/**"
      - "**/secrets/**"
    denied_commands:
      - "rm -rf"
      - "git push --force"
    max_diff_files: 25
    require_disclosure: true

    terraform (adds): "**/*.tfstate", "iam/**" to protected_paths; adds "terraform apply" to denied_commands.

    kubernetes (adds): "**/rbac/**", "**/*secret*.yaml" to protected_paths; adds "kubectl delete" to denied_commands.

    blank: same shape, empty protected_paths/denied_commands — for a team that wants to author its own list from the CODEOWNERS-mandatory paths onward.

  3. Copy examples/ai-governance/evaluate.sh into .ai-governance/evaluate.sh in the target repo, executable bit set (chmod +x).

  4. Check for yq v4 (command -v yq && yq --version). If it is missing, stop and ask the user to install it: Homebrew (brew install yq) on macOS, their distribution's package, or a release binary from github.com/mikefarah/yq/releases checked against its published checksum. Never install it yourself. It needs sudo, and this binary parses the file that decides what the agent may touch, so where it comes from is the user's call. In CI, ubuntu-latest already ships yq v4, and examples/ai-governance/ai-governance-check.yml pins and verifies its own copy.

  5. Write .github/hooks/preToolUse.json:

    {
      "version": 1,
      "hooks": {
        "preToolUse": [
          {"type": "command", "bash": ".ai-governance/evaluate.sh --mode=hook --platform=copilot --event=preToolUse", "timeoutSec": 10}
        ]
      }
    }

    And .github/hooks/postToolUse.json (same shape, --event=postToolUse). This one is not a second gate. --event=postToolUse short-circuits before the policy is even loaded: it appends one completed line to the audit log, emits no decision, and exits 0. It never re-evaluates protected_paths or denied_commands — the call has already run by then, and re-deciding would duplicate the audit line preToolUse already wrote for the same call under audit or warn:

    {
      "version": 1,
      "hooks": {
        "postToolUse": [
          {"type": "command", "bash": ".ai-governance/evaluate.sh --mode=hook --platform=copilot --event=postToolUse", "timeoutSec": 10}
        ]
      }
    }

    On Claude Code, prefer the plugin: with platform-guardrails@platform-skills enabled, the PreToolUse/PostToolUse gate already ships from the plugin and switches on in any project that commits .ai-governance.yaml. Skip the settings merge below in that case (a second wiring would evaluate every call twice), and keep only the policy file and .ai-governance/ assets in the repository.

  6. Merge, never overwrite .claude/settings.json.

    Claude Code's settings.json hook registration is three levels deep, not two: each event key holds an array of matcher-group objects, and each matcher group holds its own inner hooks array of handler objects. matcher is optional — omitting it means "match every tool call", which is what this policy wants (every call gets evaluated, not just one tool):

    {
      "hooks": {
        "PreToolUse": [
          {
            "hooks": [
              {"type": "command", "command": ".ai-governance/evaluate.sh --mode=hook --platform=claude --event=PreToolUse"}
            ]
          }
        ]
      }
    }

    Merge into that structure with jq, appending a new matcher group whether or not the event key already has entries, and skipping the append if this exact command is already registered so re-running generate is idempotent:

    PRE_CMD='.ai-governance/evaluate.sh --mode=hook --platform=claude --event=PreToolUse'
    POST_CMD='.ai-governance/evaluate.sh --mode=hook --platform=claude --event=PostToolUse'
    
    mkdir -p .claude
    [[ -f .claude/settings.json ]] || echo '{}' > .claude/settings.json
    
    jq --arg pre "$PRE_CMD" --arg post "$POST_CMD" '
      def add_group($event; $cmd):
        if [ (.hooks[$event] // [])[] | (.hooks // [])[]? | select(.command == $cmd) ] | length > 0
        then .
        else .hooks[$event] = ((.hooks[$event] // []) + [{"hooks": [{"type": "command", "command": $cmd}]}])
        end;
      add_group("PreToolUse"; $pre) | add_group("PostToolUse"; $post)
    ' .claude/settings.json > .claude/settings.json.tmp \
      && mv .claude/settings.json.tmp .claude/settings.json

    (.hooks[$event] // []) is what makes this safe on a file that has no hooks key, has hooks but not this event, or already has other matcher groups registered for this event. Never write .hooks.PreToolUse = [...] — that discards any matcher group the user already had.

  7. Write .github/workflows/ai-governance-check.yml (copy from examples/ai-governance/ai-governance-check.yml) — this fetches the base branch's copy of the evaluator and policy before running, not the PR's own copy (see references/ai-governance.md's "Trusted verifier" section for why).

    Bootstrap ordering — state this explicitly, don't let the user find out from a failing check: the workflow's git show origin/<base>:.ai-governance/evaluate.sh has nothing to read until a copy of the evaluator and policy already exists on the base branch. The very first PR that runs generate is, by definition, the PR that would create those files — so if that same PR also enables the required check, the check fails on its own PR before anything is on main to fetch. Sequence it in two stages:

    1. Land .ai-governance.yaml, .ai-governance/evaluate.sh, the hook configs, and the workflow file in a normal PR, merged without the check marked as required yet — it will run (and may fail-open on the git show, which is fine for this one merge) but nothing blocks on it.
    2. Only after that PR is on the base branch, do step 9's branch-protection change, making the check required from that point on. Every PR after this one has a real base-branch copy to fetch.
  8. Add .ai-governance/audit.log to .gitignore — it can contain command text and file paths that may carry secrets, and must never be committed:

    grep -qxF '.ai-governance/audit.log' .gitignore 2>/dev/null || echo '.ai-governance/audit.log' >> .gitignore
  9. Print the exact branch-protection step (Settings → Branches → the target branch's ruleset → require the ai-governance check) and the exact CODEOWNERS lines to add — and print it as a follow-up step to run after the bootstrap PR above has merged, not alongside this PR:

    .ai-governance.yaml           @your-org/platform-team
    .ai-governance/               @your-org/platform-team
    .github/hooks/                @your-org/platform-team
    .github/workflows/ai-governance-check.yml  @your-org/platform-team
    .claude/settings.json         @your-org/platform-team

    State explicitly: this command cannot set branch protection or write CODEOWNERS itself — no repo-admin scope is assumed.

Validation:

printf '%s\0' ".github/workflows/release.yml" | .ai-governance/evaluate.sh --mode=ci --policy=.ai-governance.yaml
# Under enforcement: block → {"permissionDecision":"deny","permissionDecisionReason":"..."}, exit 2
# Under enforcement: warn  → {"permissionDecision":"allow"}, plus a would_deny_warn line in .ai-governance/audit.log
#                            (the workflow reads those lines and posts the PR comment)
# Under enforcement: audit → {"permissionDecision":"allow"}, plus a would_deny line in .ai-governance/audit.log

One exception to the tier table, worth stating when you hand this over: a diff that touches a governance asset (.ai-governance.yaml, .ai-governance/**, .github/hooks/**, .github/workflows/ai-governance-check.yml, .claude/settings.json) and another protected path denies under every tier, audit included. Otherwise a single PR could switch off the gate and change what it was guarding at the same time. Changing a governance asset on its own is an ordinary protected_paths hit and follows the tier as normal.

Mode: check

Dry-run the evaluator against a path, a command string, or a diff range, without a live agent session.

Steps:

  1. Accept a path (file edit test), a command string (bash test), or a diff range (check --diff main...HEAD).
  2. Always pass --platform=none. That is the evaluator's platform-neutral renderer: it prints {"decision":…,"rule":…,"reason":…} instead of a Copilot or Claude envelope, because a platform engineer tuning policy needs the rule that fired, not a vendor transport format. It also writes nothing to .ai-governance/audit.log, so a dry run never leaves fake entries in the real audit trail:
    # File edit test — an edit payload, so this is write intent
    echo '{"toolName":"edit","toolArgs":{"path":".github/workflows/release.yml","content":"x"}}' \
      | .ai-governance/evaluate.sh --mode=hook --platform=none
    # → {"decision":"deny","rule":"protected_paths","reason":"protected_paths: .github/workflows/** (.github/workflows/release.yml)"}
    
    # Command string test — denied_commands, not protected_paths
    echo '{"toolName":"bash","toolArgs":{"command":"terraform apply -auto-approve"}}' \
      | .ai-governance/evaluate.sh --mode=hook --platform=none
    # → {"decision":"deny","rule":"denied_commands","reason":"denied_commands: terraform apply"}
    
    # Diff range test — every rule evaluated in one pass, codes joined and deduped.
    # --base-ref is required here, not optional: without it, `require_disclosure`
    # has no commit range to inspect and is itself a violation (see point 4 below),
    # not a silently-skipped rule.
    git diff -z --name-only main...HEAD \
      | .ai-governance/evaluate.sh --mode=ci --platform=none --base-ref=main
    # → {"decision":"deny","rule":"protected_paths,max_diff_files","reason":"protected_paths: iam/** (iam/policy.json); max_diff_files: 31 > 25"}
    Exit code is 0 for allow and 2 for deny, so this is scriptable.
  3. Report each decision with the matching rule (which protected_paths glob or denied_commands entry fired), so the policy can be tuned before rollout.
  4. Three things to say out loud when reporting, because they change what the result means:
    • A deny here is what the rules say, not what the current tier does. --platform=none reports the tier-resolved decision, so under enforcement: audit or warn a matching rule renders as allow — run check against a copy of the policy with enforcement: block to see the rule set on its own.
    • protected_paths only fires on write intent. A Read against a protected path is allowed by design, so testing with a read-shaped payload will always come back allow.
    • If require_disclosure: true and --base-ref is omitted from a --mode=ci invocation, that is itself a require_disclosure finding — not a rule that silently didn't run. Always pass --base-ref when disclosure enforcement is on.

Mode: audit

Scan repos in an org for policy presence, enforcement tier, and hook/policy drift — no infrastructure required.

Steps:

  1. Accept an org or list of repos (gh api via existing auth, same pattern as triage.md's comment-fetching).
  2. For each repo: check for .ai-governance.yaml presence, current enforcement tier, and whether the generated hook files match what the current policy would generate (hash comparison, not full regeneration): Ask the API for the raw file rather than fetching base64 .content and decoding it — the decode flag is -d on GNU coreutils and -D on macOS/BSD, so a decode step here is a portability bug waiting on whoever runs it next:
    gh api "orgs/<org>/repos" --paginate --jq '.[].full_name' | while read -r repo; do
      policy="$(gh api "repos/$repo/contents/.ai-governance.yaml" \
        -H "Accept: application/vnd.github.raw" 2>/dev/null)"
      if [[ -z "$policy" ]]; then
        echo "$repo: not adopted"
      else
        tier="$(echo "$policy" | yq eval '.enforcement' -)"
        echo "$repo: enforcement=$tier"
      fi
    done
  3. Report: adoption rate, tier breakdown, and any repo where hooks are stale relative to its own policy file.

This is the fleet-visibility value normally associated with durable audit ingestion (Phase 4), delivered here via gh api with zero infrastructure — it degrades in reach, not in kind, without it.

Mode: explain

Translate an existing .ai-governance.yaml into plain English — same shape as opa.md's explain mode.

Steps:

  1. Read the policy file.
  2. Describe in plain language: what paths are protected and why they matter, what commands are denied and their risk, what enforcement tier is active and what a developer will actually see when a rule fires under that tier, and whether require_disclosure is active.
  3. If enforcement: audit, explicitly say so: "no rule currently blocks anything — violations are logged only."
Repository

nitinjain999/platform-skills

Path

skills/ai-governance/SKILL.md

Last updated
First committed

Also appears in

nitinjain999/platform-skills
In sync

since Oct 19, 2026

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.