Akshay Parkhi's Weblog

Subscribe

10 Hidden Concepts in Strands SDK Hooks That You Won’t See by Reading the Code

9th March 2026

The Strands SDK hook system looks simple on the surface — register a callback, receive an event. But there are 10 hidden concepts buried in the design that you’ll never see by just reading the code. Here’s what’s actually happening under the hood.

1. The Event Object is a Two-Way Channel

It looks like you’re just passing data to a callback. But the event object goes both directions:

Event Loop  ——creates event——→  Hook Function
Event Loop  ←——reads modified event——  Hook Function

The hook doesn’t return anything. It modifies the object in place. This works because Python objects are passed by reference — the event loop and the hook are looking at the same object in memory.

before_event = BeforeToolCallEvent(cancel=False)  # event loop creates it
hooks.invoke(before_event)                         # hook sets cancel=True on SAME object
if before_event.cancel:                            # event loop reads the SAME object
    # tool is blocked

If events were immutable (like strings or tuples), the entire hook system wouldn’t work. The mutability IS the mechanism.

2. Hooks Have No Return Value — By Design

Every hook function returns None. This is intentional. If hooks returned values, who wins when two hooks disagree?

# WRONG approach (not how it works):
result1 = hook_a(event)  # returns "allow"
result2 = hook_b(event)  # returns "block"
# Now what? Who wins?

# ACTUAL approach (last writer wins):
hook_a(event)  # sets event.cancel = False
hook_b(event)  # sets event.cancel = True  ← this wins because it ran last

Last hook to modify the event wins. Registration order matters. This is why the Strands SDK runs “After” events in reverse — so the first-registered hook (usually the most important one) gets the last word during cleanup.

3. Reverse Order is a Stack (Setup/Teardown Pattern)

Think of getting dressed and undressed:

MORNING (setup — forward order):
  1. Put on underwear
  2. Put on pants
  3. Put on shoes

NIGHT (cleanup — REVERSE order):
  3. Take off shoes      ← must happen FIRST
  2. Take off pants
  1. Take off underwear  ← must happen LAST

You can’t take off underwear before pants. Same with code:

# Setup hooks (Before events — forward order):
hook_A: open database connection
hook_B: start a transaction (needs the connection from A)
hook_C: start a timer

# Cleanup hooks (After events — REVERSE order):
hook_C: stop the timer
hook_B: commit the transaction (connection still open!)
hook_A: close database connection (transaction already committed!)

If cleanup ran forward (A→B→C), hook_A would close the database connection BEFORE hook_B could commit the transaction. Data lost. Reverse order guarantees each hook can clean up what it set up.

4. Hooks Can Talk to Each Other (Through the Event)

Two hooks registered for the same event share the same event object. They communicate through it like a shared whiteboard:

def guard_1(event: BeforeToolCallEvent):
    """Scans for suspicious keywords."""
    if "drop table" in str(event.tool_input):
        event.cancel_reason = "SQL injection detected"
        # Flags it but does NOT cancel (wants second opinion)

def guard_2(event: BeforeToolCallEvent):
    """Makes the final decision."""
    if event.cancel_reason:              # reads what guard_1 wrote
        event.cancel = True              # guard_2 makes the call to block

Guard 1 and guard 2 don’t know each other exist. They never call each other. But they communicate through the event object — like two people writing sticky notes on the same package.

5. The Model Never Knows Hooks Exist

When a hook blocks divide(10, 0), the model gets back:

{"toolResult": {"content": [{"text": "Tool cancelled: Division by zero blocked"}]}}

The model thinks the tool ran and returned an error. It doesn’t know a hook intercepted it. From the model’s perspective, it asked for a tool and got a result back. The hook is invisible to the AI.

This means hooks can enforce rules the model cannot override or argue with. The model can’t say “please run it anyway” — it never knows the tool didn’t run.

6. AfterInvocationEvent ALWAYS Fires (Even on Crash)

try:
    result = event_loop_cycle(messages, hooks)
    hooks.invoke(AfterInvocationEvent(agent=agent, result=result))
except Exception as e:
    hooks.invoke(AfterInvocationEvent(agent=agent, result=None, error=e))  # STILL fires
    raise

This is the finally pattern. Cleanup hooks MUST run regardless of success or failure. If a hook opened a database connection in BeforeInvocationEvent, it needs AfterInvocationEvent to close it even if the agent crashed. Without this guarantee, hooks would leak resources on every error.

7. BeforeModelCallEvent Can Rewrite Reality

The most powerful hidden concept. BeforeModelCallEvent gives you messages and tool_specs as mutable lists. You can rewrite what the model sees:

Example: PII Redaction

def redact_pii(event: BeforeModelCallEvent):
    """Runs BEFORE the model sees the messages."""
    import re
    for msg in event.messages:
        for block in msg["content"]:
            if "text" in block:
                block["text"] = re.sub(
                    r'\d{3}-\d{2}-\d{4}', '[SSN REDACTED]', block["text"]
                )

# What happens:
# User typed:     "My SSN is 123-45-6789, what tax bracket am I in?"
# Model receives: "My SSN is [SSN REDACTED], what tax bracket am I in?"
# The model NEVER sees the SSN.

Example: Hide Dangerous Tools at Night

def remove_tool_at_night(event: BeforeModelCallEvent):
    import datetime
    if datetime.datetime.now().hour >= 22:
        event.tool_specs = [
            t for t in event.tool_specs
            if t["toolSpec"]["name"] != "delete_file"
        ]
    # Model can't use delete_file because it doesn't know it exists.
    # Like hiding the scissors from a child.

Example: Inject Context from a Database

def inject_context(event: BeforeModelCallEvent):
    user_profile = get_from_database("current_user")
    event.messages.insert(0, {
        "role": "user",
        "content": [{"text": f"Context: User is {user_profile['name']}, "
                              f"account type: {user_profile['plan']}"}]
    })
    # Model thinks the user provided this context.
    # Actually the hook injected it from a database.

The key insight: event.messages and event.tool_specs are what gets sent to Bedrock. If you change them in the hook, the model sees your version, not the original. You control what reality the model lives in.

8. MessageAddedEvent is the Audit Trail No One Notices

It fires on EVERY message — user input, model response, tool results. It looks boring:

hooks.invoke(MessageAddedEvent(agent=None, message=tool_result_message))

But this single event enables:

  • Conversation persistence — save every message to a database as it happens
  • Real-time streaming — push each message to a WebSocket/SSE endpoint
  • Compliance logging — immutable audit trail of every AI interaction
  • Session management — the Strands SessionManager hooks into exactly this event

It’s the most frequently fired event and the one that enables the most infrastructure.

9. The Hook System IS the SDK

In the real Strands SDK, these built-in features are ALL implemented as hooks:

ConversationManager  → hooks into MessageAddedEvent to trim old messages
SessionManager       → hooks into MessageAddedEvent to persist to disk/S3
ModelRetryStrategy   → hooks into AfterModelCallEvent, sets retry=True on throttle
Plugins              → register hooks via HookProvider interface

They’re not special internal code. They use the same hooks.add_callback() you do. If you removed hooks, you’d lose conversation management, session persistence, retry logic, and plugins. The SDK would be just the bare event loop.

10. One Hook Can Transform the Entire Agent

# Read-only agent (can think but never act):
def block_all_tools(event: BeforeToolCallEvent):
    event.cancel = True

# Structured-output agent:
def force_json(event: BeforeModelCallEvent):
    event.messages.insert(0, {
        "role": "user",
        "content": [{"text": "You must respond in JSON only."}]
    })

# Emergency stop:
def emergency_stop(event: BeforeModelCallEvent):
    if some_external_condition():
        event.messages.clear()  # empty messages → model call fails → agent stops

Hooks transform the agent’s fundamental behavior without changing any agent code. A single hook can make an agent read-only, force structured output, or shut it down entirely.

The Interception Pattern

Every hook follows the same interception pattern:

Model decides → "run divide(10, 0)"
                    ↓
              BeforeToolCallEvent fires
                    ↓
              hook sets cancel=True
                    ↓
              divide() NEVER CALLED
                    ↓
              Error text sent back to model
                    ↓
              Model explains gracefully

The hook is a gatekeeper sitting between the model’s intent and your code’s execution. The model has no safety — it will happily request divide by zero. Your hook catches it before damage happens.

Key Concepts Summary

ConceptOne-Line Explanation
Mutable eventsHooks modify the event object in place — no return value needed
No return valueLast hook to write wins; avoids “who wins?” conflicts
Reverse cleanupAfter events run in reverse so each hook can clean up what it set up
Event as blackboardHooks communicate through shared event properties, not direct calls
Model blindnessThe AI never knows hooks exist — it can’t override them
Guaranteed cleanupAfterInvocationEvent fires even on crash (like finally)
Reality rewritingBeforeModelCallEvent lets you change what the model sees
Audit trailMessageAddedEvent fires on every message — enables persistence and logging
Hooks ARE the SDKCore features (sessions, retries, plugins) are built on hooks
Agent transformationOne hook can make an agent read-only, JSON-only, or shut it down

This is 10 Hidden Concepts in Strands SDK Hooks That You Won’t See by Reading the Code by Akshay Parkhi, posted on 9th March 2026.

Next: From Webcam to Robot Brain: How Vision-Language Models and Vision-Language-Action Models Actually Work

Previous: Top Claude Code Skills: What 20 YouTube Videos and 2.3M Views Agree On