A GitHub CLI extension that runs local workflows triggered by GitHub Copilot agent hooks — like GitHub Actions, but for your AI pair programming sessions. Enforce governance, quality gates, and safety checks in real-time.
gh extension install htekdev/gh-hookflow
gh hookflow registerThat's it. Hookflow is now active for every repo you open with Copilot CLI. No per-repo setup needed.
Now open Copilot CLI and ask:
Create a hookflow to prevent the creation of .env files
The agent knows how to create hookflow rules — the register command installs an agent skill that teaches it the syntax, patterns, and best practices.
- Installs personal hooks at
~/.copilot/hooks/hooks.json— runs hookflow on every tool call across all repos - Installs agent skill at
~/.copilot/skills/hookflow/SKILL.md— teaches Copilot how to write hookflow rules
- GitHub CLI (
gh) installed and authenticated - PowerShell Core (
pwsh) installed (workflow steps run in pwsh for cross-platform consistency)
GitHub Copilot CLI hooks can block tool calls before they happen — but they can't block after. Post-hook output is ignored by the Copilot CLI. This means if you validate a file after creation and it fails, you have no way to tell the agent.
gh-hookflow solves this. It implements a post-error feedback loop that forces the agent to acknowledge and fix issues caught by post-lifecycle workflows:
- A post-lifecycle workflow validates content after the agent creates/edits a file
- If validation fails, hookflow writes an error file and blocks all subsequent tool calls
- The deny message tells the agent to read the error file for details
- The agent reads the error file (allowed through as a primitive exemption)
- The error auto-clears, and the agent can retry with the correct approach
This turns post-lifecycle hooks into blocking validators — something the Copilot CLI hooks architecture doesn't natively support.
gh-hookflow lets you run "shift-left" DevOps checks during AI agent editing sessions. Instead of waiting for CI to catch issues on pull requests, you can:
- Block dangerous edits in real-time (e.g., .env file modifications)
- Validate content after creation and force the agent to fix it
- Lint code as the agent writes it
- Enforce commit message conventions
- Run security scans before code leaves the local machine
- Guard git push — all pushes go through governance workflows
Hookflow supports two workflow formats. Both live in .github/hookflows/:
Markdown files with YAML frontmatter. Simple, declarative, no shell scripting needed. Best for pattern matching and simple governance.
---
name: block-env-files
description: Prevent creation or editing of .env files
event: file
action: block
conditions:
- field: file_path
operator: regex_match
pattern: \.env
lifecycle: pre
---
⚠️ **Sensitive File Blocked**
Cannot create or edit `.env` files. Use environment variables or a secrets manager instead.Full workflow files with shell steps, expressions, and multi-step pipelines. Use when you need scripting logic, conditional steps, or complex validation.
name: Validate API Spec
on:
file:
lifecycle: post
paths: ['api-spec.json']
types: [create, edit]
blocking: true
steps:
- name: Validate schema
run: |
$spec = Get-Content "${{ event.file.path }}" | ConvertFrom-Json
if (-not $spec.response_schema) {
Write-Error "api-spec.json must include response_schema"
exit 1
}When to use which: Start with hookify rules. Move to YAML workflows only when you need shell commands, multi-step pipelines, conditional logic (
if:expressions), or step outputs.
After registering globally, add workflows to specific repos:
cd your-project
gh hookflow init --repo # scaffolds .github/hookflows/ with an example workflowOr just ask Copilot to create one — it knows the syntax from the installed skill.
| Command | Scope | What it does |
|---|---|---|
gh hookflow register |
Global (all repos) | Installs personal hooks + agent skill in ~/.copilot/. Run once. |
gh hookflow init |
Per-repo | Creates .github/hooks/hooks.json for repo-level hooks. |
gh hookflow init --repo |
Per-repo | Also scaffolds .github/hookflows/ with example workflow. |
When both exist, repo hooks run first and personal hooks automatically defer (via the --global flag). This means repo-specific workflows always take priority.
gh hookflow test --event file --action edit --path ".env" # test locally
git add .github/ && git commit -m "Add hookflow workflows" # share with teamTeam members just need gh extension install htekdev/gh-hookflow && gh hookflow register to run your workflows during their Copilot sessions.
| Command | Description |
|---|---|
gh hookflow register |
Register personal hooks and agent skill (global, all repos) |
gh hookflow register --unregister |
Remove personal hooks and skill |
gh hookflow init |
Initialize per-repo hooks |
gh hookflow init --repo |
Also scaffold example workflows |
gh hookflow create <prompt> |
Create a workflow using AI |
gh hookflow discover |
List workflows in the current directory |
gh hookflow validate |
Validate workflow files |
gh hookflow test |
Test a workflow with a mock event |
gh hookflow run |
Run workflows (used by hooks internally) |
gh hookflow git-push |
Push with pre/post governance workflows |
gh hookflow logs |
View gh-hookflow debug logs |
gh hookflow triggers |
List available trigger types |
gh hookflow version |
Show version information |
gh-hookflow integrates with GitHub Copilot CLI hooks:
┌─────────────────────────────────────────────────────────────┐
│ Copilot Agent Session │
│ │
│ User: "Edit the .env file" │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────┐ │
│ │ preToolUse Hook │ │
│ │ └─> gh hookflow run --event-type pre │ │
│ │ └─> Matches .github/hookflows/*.yml │ │
│ │ └─> Runs blocking workflow │ │
│ │ └─> Returns: deny/allow │ │
│ └──────────────────────────────────────────┘ │
│ │ │
│ ┌─────────┴─────────┐ │
│ │ │ │
│ DENIED ALLOWED │
│ │ │ │
│ Agent stops Tool executes │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────┐ │
│ │ postToolUse Hook │ │
│ │ └─> gh hookflow run --event-type post │ │
│ │ └─> Runs validation/linting │ │
│ │ └─> If blocking step fails: │ │
│ │ └─> Writes session error │ │
│ │ └─> BLOCKS next tool call │ │
│ │ └─> Agent reads error file │ │
│ │ └─> Error auto-clears │ │
│ └──────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
GitHub Copilot CLI hooks ignore postToolUse output — there's no native way to give the agent feedback after a tool runs. gh-hookflow works around this with a session error file:
- Post-lifecycle workflow fails → hookflow writes
error.mdto the session directory - Next preToolUse → hookflow detects the error file and denies with: "Read the error file at {path} to acknowledge it"
- Agent reads the error file → hookflow allows the
viewthrough (primitive exemption) - postToolUse for the view → hookflow deletes the error file
- Next tool call → no error file exists, agent proceeds normally
The agent learns what went wrong and can fix it — turning a passive post-hook into an active feedback loop.
# One-time global setup (personal hooks + skill for all repos)
gh hookflow register
# Initialize a repository with example workflows
gh hookflow init --repo
# Discover workflows in the current directory
gh hookflow discover
# Validate workflow files
gh hookflow validate
# Test a workflow with a mock commit event
gh hookflow test --event commit --path src/app.ts
# Test a workflow with a mock file event
gh hookflow test --event file --action edit --path src/app.ts
# View logs for debugging
gh hookflow logs
gh hookflow logs -f # Follow mode (like tail -f)Workflows are defined in .github/hookflows/*.yml:
name: Block Sensitive Files
description: Prevent edits to sensitive files
on:
file:
lifecycle: pre # Run BEFORE the action (can block)
paths:
- '**/*.env*'
- '**/secrets/**'
paths-ignore:
- '**/*.md'
types:
- edit
- create
blocking: true # Exit 1 = deny the action
steps:
- name: Deny edit
run: |
echo "❌ Cannot edit sensitive files"
exit 1lifecycle: pre(default) — Runs BEFORE the tool executes. Can block/deny the operation.lifecycle: post— Runs AFTER the tool executes. For validation, linting, notifications.
Post-lifecycle workflows can be blocking — if a step fails, hookflow writes a session error that blocks all subsequent tool calls until the agent reads and acknowledges the error.
# Post-edit validation — blocks agent until fixed
name: Validate API Spec
on:
file:
lifecycle: post
paths: ['api-spec.json']
types: [create, edit]
blocking: true
steps:
- name: Validate schema
run: |
$spec = Get-Content "${{ event.file.path }}" | ConvertFrom-Json
if (-not $spec.response_schema) {
Write-Error "api-spec.json must include response_schema"
exit 1
}# Post-edit linting — non-blocking, just report
on:
file:
lifecycle: post
paths: ['**/*.ts']
types: [edit]
blocking: false
steps:
- name: Lint TypeScript
run: npx eslint "${{ event.file.path }}" --fix| Trigger | Description | Example |
|---|---|---|
file |
File create/edit/delete events | Block .env edits |
tool |
Specific tool calls with arg patterns | Block rm -rf commands |
commit |
Git commit events | Require tests with source changes |
push |
Git push events | Require PR for main branch |
hooks |
Match by hook type | Run on all preToolUse |
Supports ${{ }} expressions with GitHub Actions parity:
steps:
- name: Conditional step
if: ${{ endsWith(event.file.path, '.ts') }}
run: echo "TypeScript file: ${{ event.file.path }}"| Expression | Description |
|---|---|
event.file.path |
Path of file being edited |
event.file.action |
Action: edit, create, delete |
event.file.content |
File content (for create) |
event.tool.name |
Tool name being called |
event.tool.args.* |
Tool argument values |
event.commit.message |
Commit message |
event.commit.sha |
Commit SHA |
event.lifecycle |
Hook lifecycle: pre or post |
env.MY_VAR |
Environment variable |
| Function | Description |
|---|---|
contains(search, item) |
Check if string/array contains item |
startsWith(str, value) |
String starts with value |
endsWith(str, value) |
String ends with value |
format(str, ...args) |
String formatting |
join(array, sep) |
Join array to string |
toJSON(value) |
Convert to JSON string |
fromJSON(str) |
Parse JSON string |
always() |
Always true |
success() |
Previous steps succeeded |
failure() |
Previous step failed |
transcript() |
Full session transcript as JSON array |
transcript('regex') |
Transcript entries matching regex |
transcript_since('regex') |
Entries after last match of regex |
transcript_count('regex') |
Count of entries matching regex |
transcript_last('regex') |
Last entry matching regex |
name: Block Sensitive Files
on:
file:
paths: ['**/.env*', '**/secrets/**', '**/*.pem', '**/*.key']
types: [edit, create]
blocking: true
steps:
- name: Deny
run: |
echo "❌ Cannot modify: ${{ event.file.path }}"
exit 1name: Require Tests
on:
commit:
paths: ['src/**']
paths-ignore: ['src/**/*.test.*']
blocking: true
steps:
- name: Check for test files
run: |
if ! echo "${{ event.commit.files }}" | grep -q '\.test\.'; then
echo "❌ Source changes require tests"
exit 1
finame: Lint on Save
on:
file:
lifecycle: post
paths: ['**/*.ts', '**/*.tsx']
types: [edit]
blocking: false
steps:
- name: ESLint
run: npx eslint "${{ event.file.path }}" --fixHookflow records every tool call it sees into a session transcript. Use transcript_*() functions to query what the agent has already done — enabling advisory governance (checking the agent should have done something) rather than only blocking governance.
Check tests were run before commit:
name: Suggest Tests Before Commit
on:
tool:
name: powershell
blocking: true
steps:
- name: check-tests
if: transcript_count('go test|npm test|pytest|jest') == 0
run: |
Write-Output '{"permissionDecision":"deny","permissionDecisionReason":"No test execution found this session. Consider running tests before committing."}'Check for code review since last edit:
name: Review Before Commit
on:
tool:
name: powershell
blocking: true
steps:
- name: check-review
if: transcript_count('code-review|code_review') == 0
run: |
Write-Output '{"permissionDecision":"deny","permissionDecisionReason":"Consider running a code review before committing."}'The transcript functions use regex matching across the entire serialized hook payload, so patterns like go test match even when the command is buried inside a powershell tool call's arguments. The transcript is capped at 1000 entries (configurable via HOOKFLOW_TRANSCRIPT_MAX_ENTRIES).
Hookflow enforces critical safety checks before any workflow matching:
git pushis blocked — All pushes must go throughgh hookflow git-push, which runs pre/post governance workflows- Multiple git commands in one tool call are denied — Each git operation must be a separate tool call
These guards scan raw hook input regardless of tool name and cannot be bypassed.
# Push with governance workflows
gh hookflow git-push origin mainThe push runs synchronously through 3 phases: pre-push workflows → git push → post-push workflows. The command prints the result as JSON when complete.
Enable debug logging:
# Set environment variable
export HOOKFLOW_DEBUG=1
# View logs
gh hookflow logs
gh hookflow logs -n 100 # Last 100 lines
gh hookflow logs -f # Follow mode
gh hookflow logs --path # Print log file pathLogs are stored in ~/.hookflow/logs/ with 7-day retention.
# Build
go build -o bin/gh-hookflow ./cmd/hookflow
# Test
go test ./... -v
# Test with coverage
go test ./... -coverprofile=coverage.out
go tool cover -html=coverage.outThe project includes end-to-end tests that validate hookflow against real Copilot CLI integration across platforms (Ubuntu, macOS, Windows).
15 test scenarios covering:
- Workflow validation and discovery
- Sensitive file blocking (
.env,.key,.pem,.cert) - Normal file operations (allow by default)
- Post-lifecycle hooks (blocking and non-blocking)
- Git commit governance (conventional commit format)
- Content enforcement (e.g., block
console.login production code) - Continue-on-error step behavior
- Paths-ignore filtering
- Multi-step pipelines with
failure()/always()expressions - Step timeout enforcement
- Primitive guards (git push block, multi-git deny)
- Post-error feedback loop — validates the full cycle: post-hook fails → agent is blocked → reads error → fixes the issue
- Copilot CLI integration (
copilot -pprogrammatic mode, requiresCOPILOT_GITHUB_TOKEN)
Running locally with hookflow run --raw:
# Block test — should return permissionDecision: deny
echo '{"toolName":"create","toolArgs":{"path":".env","file_text":"SECRET=x"},"cwd":"'$(pwd)'"}' \
| hookflow run --raw --event-type preToolUse
# Allow test — should return permissionDecision: allow
echo '{"toolName":"create","toolArgs":{"path":"hello.txt","file_text":"Hello"},"cwd":"'$(pwd)'"}' \
| hookflow run --raw --event-type preToolUseCI setup: The E2E workflow (.github/workflows/e2e.yml) requires a COPILOT_GITHUB_TOKEN repository secret (fine-grained PAT with Copilot Requests permission) for the Copilot CLI integration tests. The direct hookflow run --raw tests run without any secrets.
- GitHub Copilot CLI — The AI coding assistant this extends
- Copilot Hooks Documentation — Official hooks reference
MIT