An AgentCore lifecycle-hook denial means four different things depending on where it fires. Only before_invocation and before_tool_call can prevent work; after_tool_call can contain the next step but cannot undo the completed tool, and after_invocation can only report what already happened.
Put the control at the last reversible point
AgentCore harness lifecycle hooks are not four interchangeable middleware slots. They sit on different sides of the action boundary.
AWS added harness lifecycle hooks in its September 2026 AgentCore release. A harness can carry up to 20 uniquely named hooks, configured on CreateHarness or UpdateHarness. The invocation cannot override that configuration.
The lifecycle-hook event contract defines the useful separation:
before_invocation: a Lambda denial stops the agent before it starts.before_tool_call: a Lambda denial skips the proposed tool call, then the agent loop continues.after_tool_call: a Lambda denial keeps the completed tool result and stops the invocation before the next step.after_invocation: the invocation is complete, so the hook can report but cannot retract streamed output.

Choose the hook by the latest moment at which the policy can still do what its name promises. An input-policy decision belongs at before_invocation. A tool-authorization decision belongs at before_tool_call. A reconciliation or compensation decision belongs after the tool. An analytics event belongs after completion.
That is the same principle behind human approval gates for AI agents: approval has to bind the exact proposal before the material action, not merely describe an event that has already happened.
Use Lambda for decisions and event buses for evidence
Only a synchronous Lambda hook can change the AgentCore loop. SNS and EventBridge are notification targets.
AWS states that SNS and EventBridge dispatch does not block the agent loop. A delivery error does not stop the invocation. The corresponding hookEvent proves that AgentCore scheduled the dispatch, not that the subscriber received or persisted it.
That creates a clean target rule:
- send a policy decision to Lambda;
- send an operational notification to SNS or EventBridge;
- never treat a bus acknowledgement as execution authority;
- never make a critical stop depend on an asynchronous consumer eventually seeing an event.
Multiple Lambda hooks on the same event run concurrently. Any denial applies. If several deny, the first denying hook in the configured list supplies the reason. The reason is useful operator evidence, but it is not a substitute for preserving every hook result in your own decision record.

For a material CRM write, for example, use before_tool_call with Lambda to evaluate the exact tool name and input. Send the resulting event to an evidence stream separately. If the evidence sink is down, the policy decision still has a defined fail behavior.
Treat the response stream as a separate egress surface
A denied inline-function call may still be visible in the response stream before the denial arrives. AWS is explicit: before_tool_call controls execution, not visibility.
This matters when model-generated tool input contains customer data, a repository secret path, a proposed payment instrument or another sensitive argument. Preventing the downstream function from running does not remove bytes already exposed to the caller consuming the stream.
Keep execution policy and stream policy separate:
- Buffer tool-request deltas until the matching hook decision is known when the proposal itself is sensitive.
- Filter fields that the client does not need, even when the tool may execute.
- Join the proposal and decision by
toolUseIdrather than arrival order. - Release the client-side inline function only when the terminal
messageStopindicatestool_use. - Log a denied proposal as proposed work, never as an attempted provider mutation unless a provider request was actually made.
The application owns that egress boundary. AgentCore's harness security model keeps caller authorization, input validation and configured capability access under the customer's responsibility. Passing IAM or JWT authentication is not the same as permission to see every generated tool argument.
Deny incomplete context explicitly
failureMode: deny does not make a context-dependent policy fail closed when the hook payload is truncated. The Lambda still receives the event and must return the decision.
AgentCore limits a hook context object to 64 KiB. For before_invocation, it keeps the newest contiguous sequence of complete messages that fits and marks the payload with truncated: true. If the newest message alone is too large, the messages list is empty. Other hook events replace their largest fields with a truncation marker and a preview.
There is a second boundary at before_invocation: the hook sees messages in the current request before the harness restores session state. A policy that needs earlier conversation turns cannot assume they are present.
Use this fail-closed sequence:
def lambda_handler(event, _context):
ctx = event.get("context", {})
if ctx.get("truncated"):
return {"decision": "deny", "reason": "context_truncated"}
if event.get("event") == "before_invocation":
decision = evaluate_current_request(ctx.get("messages", []))
return {"decision": decision, "reason": "request_policy"}
return {"decision": "deny", "reason": "unexpected_event"}If the policy needs restored session state, fetch the required authoritative state through a separately owned path or move the decision to a point where the exact proposal is available. Do not silently interpret missing history as approval.
Lambda hook timeouts default to 60 seconds and accept values from 1 through 900 seconds. The default failureMode is deny for a timeout, function error or invalid response. Keep it that way for prevention hooks unless the business owner has accepted the consequence of failing open.
Reconcile a completed effect outside the hook
An after_tool_call denial cannot roll back a provider mutation. It keeps the completed result and stops the next agent step.
For read-only tools, containment may be enough. For a material write, the hook needs an external reconciliation contract:
- the original task and proposal digest;
toolUseIdand tool name;- the provider request idempotency key;
- the result observed by AgentCore;
- an authoritative provider read-back;
- a terminal state such as observed, uncertain or divergent;
- the named compensation owner and allowed recovery action.
Inline functions need an extra caution. AWS says after_tool_call confirms that the harness processed a result supplied by the client. It does not attest that the client performed the represented external action. Authenticate the caller and reconcile the provider state before using that event as an audit receipt.
The broader agent workflow operating contract should name which effects are reversible, which need compensation, and which must never execute without exact pre-action authority.
AgentCore produces traces, logs and metrics for harness invocations through CloudWatch, according to its observability and cost controls. Those records explain activity. They do not turn a post-action denial into rollback or a client-supplied tool result into provider truth.
Make hook placement mechanically reviewable
The reference classifier below routes a proposed control to prevention, containment, observation or hold. It evaluates your external control record, not AgentCore's private implementation.
export function classifyHookBoundary(input) {
if (input.target !== "lambda") {
return input.intent === "observe"
? { route: "observe", reason: "notification-target" }
: { route: "hold", reason: "notification-cannot-decide" };
}
if (input.requiresCompleteContext && input.contextTruncated) {
return { route: "hold", reason: "hook-context-truncated" };
}
if (input.event === "before_invocation") {
if (input.requiresRestoredSessionState) {
return { route: "hold", reason: "restored-state-not-in-context" };
}
return { route: "prevent", reason: "before-agent-start" };
}
if (input.event === "before_tool_call") {
if (input.streamExposesSensitiveToolInput && !input.streamPolicyPresent) {
return { route: "hold", reason: "stream-egress-uncontrolled" };
}
return { route: "prevent", reason: "before-tool-execution" };
}
if (input.event === "after_tool_call") {
if (input.intent === "prevent") {
return { route: "hold", reason: "tool-result-already-kept" };
}
return { route: "contain", reason: "stop-next-step-and-reconcile" };
}
return input.intent === "observe"
? { route: "observe", reason: "invocation-already-complete" }
: { route: "hold", reason: "invocation-already-complete" };
}The retained dependency-free artifact passes 16 local cases. It covers asynchronous targets, restored-state dependence, truncated context, sensitive stream exposure, post-tool prevention, untrusted inline results and the complete release path. Those cases test control flow. They are not an AWS performance benchmark.
A pilot is ready only when it has a policy owner, versioned hook configuration, deny-on-failure prevention, a truncation case, a restored-state boundary test, a stream-egress policy, a toolUseId evidence join, an authoritative side-effect receipt and a compensation owner.
Frequently asked questions
Can an EventBridge lifecycle hook deny an AgentCore tool call?
No. SNS and EventBridge are non-blocking notification targets. Use a Lambda hook at before_tool_call when the loop needs a synchronous allow or deny decision.
Does an after_tool_call denial roll back the tool action?
No. AgentCore keeps the completed tool result and stops the invocation before the next step. Reconcile the provider's authoritative state and run an owned compensation path when the action is reversible.
Does failureMode deny handle truncated hook context?
No. Context truncation does not activate failureMode. The Lambda receives a truncation marker and must return an explicit decision, typically deny when the policy needs complete context.








