ToolPermissionContext.suggestions AttributeError: PermissionUpdate.from_dict() (2026)

If you write a permission callback for the Claude Agent SDK and access ctx.suggestions[0].mode, you may hit AttributeError: 'dict' object has no attribute 'mode'. Before SDK 0.16 (2026), ToolPermissionContext.suggestions contained raw dicts instead of typed PermissionUpdate instances. The fix is to convert with PermissionUpdate.from_dict(). Here is the full pattern.

Why the AttributeError happens

The Agent SDK builds ToolPermissionContext when routing a tool call through your permission callback. In some SDK versions the suggestions field is populated with plain dicts because the internal message parser did not run its type conversion pass. Any code that expects a PermissionUpdate object (with .mode, .allow_pattern, etc.) crashes on attribute access.

The SDK team added PermissionUpdate.from_dict() as a public helper so your callback can defensively convert raw dicts into typed instances.

The fix: convert with PermissionUpdate.from_dict()

from claude_agent_sdk import (
    ClaudeSDKClient,
    ClaudeAgentOptions,
    PermissionUpdate,
    ToolPermissionContext,
)

async def permission_callback(ctx: ToolPermissionContext):
    """Approve or deny tool calls based on suggested permissions."""

    # Defensive conversion: works whether suggestions are dicts or typed
    typed_suggestions = []
    for s in ctx.suggestions:
        if isinstance(s, dict):
            typed_suggestions.append(PermissionUpdate.from_dict(s))
        else:
            typed_suggestions.append(s)

    # Now .mode is safely accessible on every item
    for suggestion in typed_suggestions:
        if suggestion.mode == "allow":
            return {"decision": "allow", "updated_permissions": [suggestion]}

    return {"decision": "deny"}

options = ClaudeAgentOptions(
    model="claude-opus-4-8",
    permission_callback=permission_callback,
)
client = ClaudeSDKClient(options=options)

Debugging checklist

  • Check your SDK version. Upgrading to 0.16+ removes the need for manual conversion because the internal parser now emits typed instances.
  • If you cannot upgrade immediately, wrap every ctx.suggestions access with the isinstance-check pattern above.
  • Log the type of each suggestion during dev to catch mixed lists early: print([type(s).__name__ for s in ctx.suggestions]).
  • If PermissionUpdate.from_dict raises TypeError, the source dict is missing required fields. Log the raw dict for inspection.

Real-world example: read-only sandbox agent

from claude_agent_sdk import (
    ClaudeSDKClient,
    ClaudeAgentOptions,
    PermissionUpdate,
)

WRITE_TOOLS = {"write", "shell", "delete", "rm"}

async def readonly_callback(ctx):
    """Deny any write-side tool call in a read-only sandbox."""

    if ctx.tool_name in WRITE_TOOLS:
        return {"decision": "deny", "reason": "readonly sandbox"}

    # Convert suggestions safely
    typed = [
        PermissionUpdate.from_dict(s) if isinstance(s, dict) else s
        for s in ctx.suggestions
    ]

    # Prefer the strictest allow suggestion
    allow_suggestions = [s for s in typed if s.mode == "allow"]
    if allow_suggestions:
        return {
            "decision": "allow",
            "updated_permissions": [allow_suggestions[0]]
        }

    return {"decision": "allow"}   # default allow for reads

options = ClaudeAgentOptions(
    model="claude-opus-4-8",
    permission_callback=readonly_callback,
    allowed_tools=["read", "grep", "web_search"],
)
client = ClaudeSDKClient(options=options)

Common mistakes with permission callbacks

  • Assuming suggestions is always typed. As of mid-2026 the SDK is still stabilizing. Defensive dict conversion adds one line and prevents crashes.
  • Returning None from the callback. The SDK expects a dict with a decision key. Returning None crashes downstream tool routing.
  • Skipping updated_permissions in an allow response. If you allow, pass the suggestion back so subsequent calls inherit the permission grant.
  • Logging inside a hot callback. Permission callbacks run per tool invocation. Log at debug level, not info, or you flood logs during long agent runs.

Debugging checklist for the Anthropic SDK error

Before diving into fixes, run through this diagnostic checklist. Nine times out of ten the answer surfaces here.

  1. Read the full traceback, not just the error message. The stack trace shows exactly which line and which call chain triggered the error. The last line names the immediate cause; earlier lines show how you got there.
  2. Add print or debug statements just before the failing line. Print the variable, its type, and its value. Nine out of ten error surprises come from the value being different from what you assumed.
  3. Check Python version compatibility. Errors sometimes result from APIs that changed between versions. Run your interpreter version check and compare against the library documentation for that version.
  4. Isolate the failing call in a minimal reproducer. Copy the failing line into a small standalone script with hardcoded inputs. If it fails there too, the bug is in your code. If not, something in your surrounding context is contributing.
  5. Search the exact error message. Include the class name and the specific text in your search. Chances are someone else hit the same issue and the fix is documented on Stack Overflow or the library’s GitHub issues.

Common causes for the Anthropic SDK error

Most instances of this error trace back to one of these root causes:

  • Uninitialized or missing input. A variable was not populated before use, or the input source (file, API response, database row) did not contain the expected key or value.
  • Type mismatch. The code expected a specific type (dict, list, string) but received something different. Python’s dynamic typing means this often surfaces at runtime, not at compile time.
  • Version drift. The library API changed and your code assumes the old signature. Check the library’s changelog for breaking changes since the version you last used.
  • Race condition or ordering issue. Async or concurrent code sometimes tries to access data before it is ready. Add awaits, locks, or explicit ordering to fix.
  • Copy-paste from stale tutorial. Older tutorials may use APIs that no longer exist. Always check the official docs for the current version.

Testing and prevention

Preventing this class of error from recurring is more valuable than fixing it once. Build these habits into your workflow:

  • Write tests that trigger the error path. If your test suite hits the error scenario, catch and assert it. A well-written test prevents the same bug from returning.
  • Validate inputs at API boundaries. When data enters your code from external sources (HTTP requests, file uploads, database queries), validate structure and types immediately.
  • Use type hints and static analysis. Tools like mypy for Python or TypeScript for JavaScript catch many type mismatches before you run the code.
  • Log important state. Structured logging with context helps you debug production issues faster. Include enough context to reconstruct what happened.
  • Read the library changelog. Before upgrading a dependency, skim the changelog for breaking changes. Two minutes of reading saves an hour of debugging.

When to ask for help

Some errors are worth solving yourself for the learning. Others are worth asking about early. Ask for help when: the error blocks a customer-facing feature, you have spent an hour without progress, the error involves security or data integrity, or you are unsure whether your fix will introduce new bugs. Post to Stack Overflow with a minimal reproducer, or ask a senior developer on your team. Time boxes are your friend.

Frequently Asked Questions

Do I still need from_dict() after upgrading?

On SDK 0.16+ the internal parser emits typed PermissionUpdate instances directly. from_dict is still available as a public helper for cases where you build suggestions programmatically or load from JSON.

What are the valid modes for PermissionUpdate?

“allow”, “deny”, and “ask”. Allow grants the tool call. Deny blocks. Ask escalates to the user for a decision.

Can I write a permission callback in TypeScript?

Yes. The Claude Agent SDK ships in Python, TypeScript, and Node. Permission callback signatures are consistent across all three but each has its own type helpers.

Do permission callbacks add latency?

Milliseconds. Callbacks run in-process. Only external calls or long DB lookups from inside the callback introduce noticeable latency. Keep callbacks fast and stateless when possible.

Where do I log permission denials for audit?

Log inside the callback right before returning the deny decision. Include tool_name, session_id, and the reason. This gives you a clean audit trail if a compliance review asks why an agent could not do X.

Adrian Mercurio

Full-Stack Developer at PIES IT Solution

Specializes in building complete capstone projects with full documentation. Strong background in PHP/MySQL development and database design. Has personally built and tested over 30 capstone-ready projects with ER diagrams, DFDs, and chapter-by-chapter thesis documentation.

Expertise: PHP, Laravel, Database Design, Capstone Projects, C#, C, C++, Python, AI Projects  ·  View all posts by Adrian Mercurio →

Leave a Comment