Skip to main content

GitHub Copilot CLI Tips & Tricks — Part 4: Automating and enforcing policies with hooks

So far in this series we've covered modes, session management, and parallelization with /fleet. This post is about hooks — one of the more powerful features of Copilot CLI. Hooks let you inject your own shell scripts at key moments during a session, enabling everything from audit logging to blocking dangerous commands before they execute.

What are hooks?

Hooks are custom scripts that run at specific points during a Copilot CLI session. They receive structured JSON input describing what's happening at that moment — which tool is being called, what arguments it received, what the session context looks like — and can optionally respond with a JSON output that influences what Copilot does next.

The key thing that makes hooks different from just writing good prompts or instructions: hooks are deterministic. They execute your code at specific lifecycle points with guaranteed outcomes. Unlike instructions that guide agent behavior, a hook can guarantee that a dangerous command never runs, or that every session is logged to your audit system — regardless of how Copilot was prompted.

Hooks are stored as JSON files in .github/hooks/*.json in your repository. For Copilot CLI specifically, they are loaded automatically from your current working directory, so no extra configuration is needed to activate them.

The different hook types

The hook types I use the most are:

sessionStart

Fires when a new session begins or when an existing session is resumed. Receives the initial prompt, the current working directory, and a source field indicating whether this is a new or resumed session.

Good uses: initializing environments, validating project state, sending a notification that a session has started, writing a session start entry to an audit log.

{
  "version": 1,
  "hooks": {
    "sessionStart": [
      { "type": "command", "bash": "./scripts/log-session-start.sh" }
    ]
  }
}

sessionEnd

Fires when a session completes or is terminated. Mirror of sessionStart — use it to clean up temporary resources, archive session logs, or notify your team that a long-running autopilot run has finished.

preToolUse

Fires before Copilot uses any tool — bash, edit, view, and so on. The input includes the tool name and its arguments. Unlike the other hooks, preToolUse can return a decision that directly controls whether the tool runs:

{ "permissionDecision": "deny", "permissionDecisionReason": "Destructive operations require approval" }

The three possible decisions are allow, deny, and ask (which prompts the user interactively). This is how you build enforcement — blocking dangerous commands, restricting file access, requiring human review for sensitive operations.

Next to those, we have other hook types like userPromptSubmitted, postToolUse, agentStop, subagentStop and errorOccurred. I didn't find a good use case yet for these hooks, but we'll see.

Setting up your first hooks

Create a JSON file at .github/hooks/policy.json (or any name you like) in your repository:

{
  "version": 1,
  "hooks": {
    "sessionStart": [
      { "type": "command", "bash": "./.github/hooks/scripts/log-session.sh", "timeoutSec": 10 }
    ],
    "preToolUse": [
      { "type": "command", "bash": "./.github/hooks/scripts/enforce-policy.sh", "timeoutSec": 30 }
    ]
  }
}

Each hook receives its context via stdin as a JSON object. Your scripts read from stdin, process it, and optionally write a JSON response to stdout.

Here's a preToolUse hook that blocks any rm -rf command before it executes:

#!/bin/bash
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.toolName')
TOOL_ARGS=$(echo "$INPUT" | jq -r '.toolArgs')

if [ "$TOOL_NAME" = "bash" ]; then
  COMMAND=$(echo "$TOOL_ARGS" | jq -r '.command')
  if echo "$COMMAND" | grep -qE 'rm\s+-rf'; then
    echo '{"permissionDecision":"deny","permissionDecisionReason":"rm -rf is not permitted. Use a safer delete command."}'
    exit 0
  fi
fi

# Allow everything else
echo '{"permissionDecision":"allow"}'

Make the script executable with chmod +x and point your hooks config at it. Now no matter how Copilot is prompted — standard mode, autopilot, or via /fleet — this class of command will never execute.

For sessionStart and userPromptSubmitted hooks, you don't need to return a JSON response at all — any output is ignored. Just write your log entry and exit 0.

Important behaviors to know

Hook failures don't block execution. If a hook exits with a non-zero code or times out, Copilot logs the failure and moves on — it never blocks agent execution. This is intentional: a broken hook script shouldn't take down your workflow. The flip side is that you need to monitor hook failures actively.

Multiple hooks of the same type run in order. If you define multiple preToolUse hooks, they execute sequentially. If any of them returns "deny", the tool is blocked — you don't need all of them to agree.

Timeouts apply per hook. The default timeout is 30 seconds. For logging hooks this is plenty; keep your policy enforcement scripts lean and fast. The GitHub docs recommend keeping hook execution under 5 seconds where possible.

Never log secrets. Prompts and tool arguments can contain sensitive data. Apply redaction before writing anything to disk or forwarding to an external system. In production, consider forwarding hook events to a centralized logging system with proper access controls rather than writing local log files.

Testing your hooks locally

Before committing hooks to a shared repo, test them by piping sample input directly into your script:

# Simulate a preToolUse event
echo '{
  "timestamp": 1704614600000,
  "cwd": "/path/to/project",
  "toolName": "bash",
  "toolArgs": "{\"command\":\"rm -rf dist\",\"description\":\"Clean build directory\"}"
}' | ./.github/hooks/scripts/enforce-policy.sh

# Validate the output is valid JSON
./.github/hooks/scripts/enforce-policy.sh | jq .

This lets you iterate on hook logic quickly without needing a live Copilot session.

Who should be using hooks?

Individual developers can use hooks for personal productivity — auto-formatting files after edits, running a linter before changes are written, keeping a local audit trail.

But hooks become especially valuable for platform and DevOps teams who are rolling out Copilot CLI across an organization. With hooks you can:

  • Enforce org-wide security policies without relying on developers to configure them manually
  • Ensure consistent audit trails across all developer sessions
  • Block access to sensitive parts of the codebase programmatically
  • Provide clear, actionable messaging when a hook denies a tool call

Because hook config files live in .github/hooks/, they can be committed to each repo and version-controlled like any other policy — giving you a reviewable, auditable record of what policies are in place.

Wrapping up

Hooks are what take Copilot CLI from a smart coding assistant to an automation platform you can actually trust in a team or enterprise context. The preToolUse hook in particular is remarkably powerful — it gives you a programmable layer between Copilot's intentions and the actual execution, letting you enforce any policy you can express in a shell script.

In the next post, we'll look at delegation — how to hand off tasks and have Copilot work autonomously on your behalf.

More information

About hooks - GitHub Docs

Using hooks with GitHub Copilot CLI - GitHub Docs

Hooks configuration - GitHub Docs

Popular posts from this blog

Kubernetes–Limit your environmental impact

Reducing the carbon footprint and CO2 emission of our (cloud) workloads, is a responsibility of all of us. If you are running a Kubernetes cluster, have a look at Kube-Green . kube-green is a simple Kubernetes operator that automatically shuts down (some of) your pods when you don't need them. A single pod produces about 11 Kg CO2eq per year( here the calculation). Reason enough to give it a try! Installing kube-green in your cluster The easiest way to install the operator in your cluster is through kubectl. We first need to install a cert-manager: kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.5/cert-manager.yaml Remark: Wait a minute before you continue as it can take some time before the cert-manager is up & running inside your cluster. Now we can install the kube-green operator: kubectl apply -f https://github.com/kube-green/kube-green/releases/latest/download/kube-green.yaml Now in the namespace where we want t...

Azure DevOps/ GitHub emoji

I’m really bad at remembering emoji’s. So here is cheat sheet with all emoji’s that can be used in tools that support the github emoji markdown markup: All credits go to rcaviers who created this list.

Podman– Command execution failed with exit code 125

After updating WSL on one of the developer machines, Podman failed to work. When we took a look through Podman Desktop, we noticed that Podman had stopped running and returned the following error message: Error: Command execution failed with exit code 125 Here are the steps we tried to fix the issue: We started by running podman info to get some extra details on what could be wrong: >podman info OS: windows/amd64 provider: wsl version: 5.3.1 Cannot connect to Podman. Please verify your connection to the Linux system using `podman system connection list`, or try `podman machine init` and `podman machine start` to manage a new Linux VM Error: unable to connect to Podman socket: failed to connect: dial tcp 127.0.0.1:2655: connectex: No connection could be made because the target machine actively refused it. That makes sense as the podman VM was not running. Let’s check the VM: >podman machine list NAME         ...