Copilot Studio's new hooks run one of your workflows at fixed points in an agent's life, and the Pre tool use hook can block a tool call before it happens.

When a hook workflow fails, times out, or returns something the agent can't parse, the agent carries on as if the hook said nothing, so a broken policy check allows the call.

Keep the hard rule in the tool's own permissions, use the hook for the fast path, and log both sides so you can see when a call ran without its check.

What Microsoft shipped

Microsoft added hooks to Copilot Studio as a preview. The docs went up at the end of September, and they apply to agents running on what Microsoft calls the GitHub Copilot harness, the newer orchestrator that came with the Copilot Studio redesign earlier this year.

A hook has two parts, an event and a workflow. When the event fires, Copilot Studio calls the workflow, hands it details about what just happened, and reads its response back into the conversation.

There are six events. Start runs when a session begins. User prompt submitted runs on every message, before the agent sees it. Error runs when something fails outside a tool. Then there are three tool events: Pre tool use, Post tool use and After tool failure.

If you've written hooks for Claude Code or GitHub Copilot, this will look familiar. The event names are close, the inputs are close, and the idea is the same. The difference is that the action is a Power Platform workflow instead of a shell script, which puts this in the hands of makers who would never touch a settings.json file.

Why this matters

Microsoft's own comparison table makes the pitch. A tool runs when the model decides it's relevant. A hook runs every time its event fires.

That's the thing enterprise teams have been asking for. Until now, if you wanted an audit record of every call an agent made to your ERP connector, you had to hope the model remembered to call the logging tool. With a Post tool use hook scoped to all tools, the log gets written whether the model thinks about it or not.

The Pre tool use event is the interesting one. It receives the tool name and the arguments the agent wants to pass, and it can return permissionDecision set to deny, a reason, or a replacement set of arguments. It's the only event that can stop the agent. Everything else can add context or rewrite values, but it can't say no.

So the obvious design is a policy workflow on Pre tool use: refund over 500 euros, deny; email to an external domain, deny; delete on a production record, deny.

The line that changes the design

Under "Things to know," there's a bullet I'd put at the top of every design review. Hooks don't stop the agent when they fail. If the workflow errors, times out, or returns something the agent can't read, "the agent continues as though the hook returned nothing."

Combine that with how deny works. All outputs are optional, and an empty permissionDecision means allow. A deny only happens when your workflow runs to completion and returns the right field with the right value.

That makes the Pre tool use hook fail-open. If the connector your policy workflow calls is slow and the workflow times out, the refund goes through. If someone deletes a variable and the workflow throws, the email goes out. Microsoft says it plainly: don't rely on a hook as your only safeguard for a business-critical rule.

Claude Code's hooks have a similar property, where only one specific exit code blocks and a crashed script lets the call through. The difference is who writes them. A developer writing a shell hook knows what an exit code is. A maker building a refund policy in a low-code designer is reading the comparison table, seeing "every time," and assuming it means "every time, or the call doesn't happen."

Three quieter ways the check goes missing

Timeouts are the obvious failure. The docs list a few others that are easy to miss.

The workflow is shared by reference. A hook stores a pointer to the workflow, not a copy. The same workflow can be bound to several hooks and also used as a tool somewhere else. If someone edits it for its tool use and renames an output, every hook pointing at it changes too. The docs say the agent ignores a response it can't read, and a renamed permissionDecision would plausibly be exactly that.

Saved isn't published. A workflow that's saved but not published doesn't run. Same for the hook itself: it isn't live until you save the agent and publish it. A policy someone tested in the designer and never published is a policy that isn't there.

The user fields can be empty. Every hook gets a metadata object with the conversation ID, the channel, and the user's Entra object ID and display name. Any of those can be empty, and the user fields are empty when the person isn't signed in. A rule written as "deny if this user is on the restricted list" will wave through anyone whose ID is blank.

What I'd do before relying on it

Put the hard limit somewhere that fails closed. The connector's own permissions, the service account's scope, the approval step inside the business system. If the refund API itself rejects anything over 500 euros without a second approver, the hook becomes a faster, friendlier version of a rule that holds anyway.

Reconcile Pre and Post. Write an audit record in the Pre tool use hook with the conversation ID and tool name, and another in Post tool use. A Post record with no matching Pre record is a call that ran without its check. Run that query daily. It's the only way you'll find out a timeout happened, because the agent won't tell you.

Keep guard workflows separate. Don't reuse a policy workflow as a tool. Give hook workflows their own names, their own owners and tighter edit permissions than the rest of the environment.

Deny on missing identity. For sensitive tools, treat an empty userAadObjectId as a reason to block, not as a value that fails the restricted-list lookup.

Break it on purpose. In a test environment, add a delay longer than the timeout to your policy workflow and watch what the agent does. Then rename an output field and try again. If the tool runs both times, you've confirmed the behavior the docs describe, and you know which controls are carrying the weight.

Keep the checks short. Every hook is a workflow run on the critical path of a tool call. A policy that calls three connectors before it answers is more likely to time out, and a timeout is an allow.

A check that answers when it can

Hooks are a real improvement for Copilot Studio. Logging every tool call, injecting the same context at the start of every session, rewriting a tool result to strip out personal data before the model sees it, all of that used to depend on the model choosing to do it. Now it doesn't.

The blocking part is weaker than the word "every" suggests. The check runs every time, and it decides only when it finishes in time with a readable answer. Build your controls so that when it doesn't, the call still hits a wall somewhere else.