The short answer
A preToolUse hook in GitHub Copilot runs before each tool call and can answer allow, deny or ask, with a reason; it can even replace the arguments with modifiedArgs.1 Three rules decide how safe it is in practice:1
- A hook that errors (non-zero exit other than 2) denies the call.
preToolUseis fail-closed on errors. - A hook that times out lets the call run. Timeouts are always fail-open, even for
preToolUseand admin-deployed hooks. The default timeout is 30 seconds. askbecomesdenyin the cloud agent, where no user is available, and in Copilot CLI’s autopilot mode.
So: make the hook fast, make it deny rather than ask when you mean “stop,” and don’t let it depend on anything slow.
Deedglass
A tool call meets your rules before it runs
Asks to run
DELETE FROM visits;
Deletes every row in visits
Deedglass checks your rules
- Reading dataAllowed
- Deleting a whole table (DROP, TRUNCATE)Never
- Changing or deleting rows without saying which (WHERE)This one
- Other changesWait for a person
Where hooks live and where they run
Hook files are JSON. Copilot reads them from .github/hooks/*.json in the repository; the CLI also reads ~/.copilot/hooks/ (or $COPILOT_HOME/hooks/).11
| Surface | Runs hooks where | Which files | Notes |
|---|---|---|---|
| Copilot CLI | Your machine | Repository and user hooks | All events supported |
| Copilot cloud agent | An ephemeral Linux sandbox | .github/hooks/*.json in the cloned repository |
Only bash or command entries honoured; some events don’t fire; ask is treated as deny |
The cloud agent row matters for teams. A hook committed to .github/hooks/ applies to every cloud agent run on that repository, so it’s a team control, reviewed like any other code.1
The full event list includes sessionStart, userPromptSubmitted, preToolUse, postToolUse, postToolUseFailure, agentStop, permissionRequest and others.1 For safety, preToolUse is the one that can stop something before it happens.
What the hook sees and returns
Input fields for preToolUse: sessionId, timestamp, cwd, toolName and toolArgs (camelCase), or the VS Code-compatible session_id, tool_name, tool_input form.1
Output, as JSON on standard output:1
| Field | Meaning |
|---|---|
permissionDecision |
allow, deny or ask; empty output means default behaviour |
permissionDecisionReason |
Shown to the agent; required for deny |
modifiedArgs |
Replacement arguments for the tool call |
The reason is worth writing well. It goes back to the agent, and a clear reason (“Destructive database command. Ask a person to run it.”) tends to make the agent stop and report, instead of trying another spelling of the same command.
The edge cases in one table
From the reference, for preToolUse specifically:1
| Situation | What happens |
|---|---|
| Exit 0, no output | Default behaviour |
Exit 0, JSON with deny |
Denied, reason shown to the agent |
| Exit 2 | Denied, even if standard output says allow |
| Any other non-zero exit | Denied: “Denied by preToolUse hook (hook errored)” |
Timeout (default 30 s, timeoutSec) |
The call runs (fail-open) |
ask in the cloud agent |
Denied |
ask in CLI autopilot |
Denied; the agent sees the CLI couldn’t ask |
Compare with Codex CLI, which marks an ask hook as failed and runs the command, as our Codex sandbox post notes. The same hook script can behave in opposite ways across agents.
A hook we tested
.github/hooks/tool-guard.json, following the reference’s example shape:1
Code Data in JSON, the format software uses to pass records and settings.
{
"version": 1,
"hooks": {
"preToolUse": [
{ "type": "command", "bash": "./scripts/check-tool.sh", "timeoutSec": 10 }
]
}
}
scripts/check-tool.sh:
Code Commands typed in a terminal. The text around it says what happens.
#!/usr/bin/env bash
# preToolUse guard: deny destructive database commands, explain why, never hang.
input="$(cat)"
if printf '%s' "$input" | grep -Eiq '(prisma migrate reset|db push --force-reset|dropdb|DROP (TABLE|DATABASE|SCHEMA)|TRUNCATE)'; then
printf '%s\n' '{"permissionDecision":"deny","permissionDecisionReason":"Destructive database command. Ask a person to run it."}'
exit 0
fi
exit 0
We piped three sample inputs in the documented camelCase shape through it:
Code What the screen or file shows, copied as is.
exit=0 out=<empty: default behaviour>
exit=0 out={"permissionDecision":"deny","permissionDecisionReason":"Destructive database command. Ask a person to run it."}
exit=0 out={"permissionDecision":"deny","permissionDecisionReason":"Destructive database command. Ask a person to run it."}
The inputs were npm test, npx prisma migrate reset --force and psql $DATABASE_URL -c "DROP TABLE users". A run on empty input took 0.014 seconds total, far inside a 10-second timeout. The tool name in our samples is a placeholder; the script checks the whole input, so it doesn’t depend on what Copilot calls its shell tool.
Design rules for Copilot hooks
- Deny, don’t ask, for anything that must stop.
askturns intodenyin the cloud agent and autopilot anyway, and being explicit keeps behaviour the same everywhere.1 - Stay fast. A timeout lets the call through. No network calls, no package installs, no
npxthat might download on first run. - Fail loudly on bugs. A crash denies, which is the safe direction; don’t wrap the script in
|| true. - Use
modifiedArgssparingly. Rewriting a command silently (adding--dry-run, say) can surprise the agent and the reader of the log. A deny with a reason is easier to reason about. - Review hook changes like CI changes. Anyone who can edit
.github/hooks/can weaken it. Code owners on that directory help.
What a hook can’t do
A hook reads the text of a tool call. A command that runs SQL from a file, or decodes its payload at runtime, carries no dangerous text to match; we showed four such cases in our ALTER TABLE over SSH post. The cloud agent’s sandbox and the credentials you give it set the real limit: a database role that can’t drop tables can’t be talked into it. Our PreToolUse hook glossary entry and hooks across agents cover the rest.
To see which destructive commands an agent on your machine could run without a prompt, paste its config into the check page or run npx deedglass scan (how the grade works); the scan Action runs the same scan on pull requests, including ones that change .github/hooks/. To try tool-call rules in your browser, open the playground.
Questions people ask
What happens if a Copilot preToolUse hook times out?
The tool call runs. Timeouts are fail-open for every event, including preToolUse.1
Does ask work in the Copilot cloud agent?
No. It is treated as deny because no user is available to answer.1
Where do Copilot hooks go?
.github/hooks/*.json in the repository; the CLI also reads ~/.copilot/hooks/.1
Written with AI assistance and checked against our claims register.