DOCUMENTATION

Make the Assistant Ask Before Every Change

Set up your assistant so that MifBridge reads run freely and every call that changes your Unreal or Blender project waits for your yes. This page gives the exact setup for Claude Code and explains what it does and does not cover.

The goal: the assistant can look at anything (list actors, read properties, inspect a Blender scene) without interrupting you, and any call that would change the project stops and shows you the call first. You answer yes or no each time. The setup below was tested on October 2, 2026 with Claude Code 2.1.280, against MifBridge 1.1, the version on sale. Anything marked "from 1.2" is in the next release and not in 1.1.

Why a plain permission list is not enough

Claude Code's permission rules (allow, ask and deny in settings.json) decide by tool name. MifBridge's default setup gives the assistant three tools: find_tools, mif_call and run_python. Almost all work goes through mif_call, which calls any MifBridge command by name, so reading the level and deleting an actor are both a call to the same tool, mcp__mif-bridge__mif_call. A rule on that tool name cannot tell them apart.

Claude Code can match some rules on a tool's arguments, but not for MCP tools in a settings file: when it loads settings.json, it skips any MCP tool rule written with parentheses. So the working route is a small hook script that looks at which command mif_call is about to run and decides from that.

MifBridge itself has no complete list of which commands change the project. The Unreal plugin's write modes do not split reads from writes yet (see Safety and Write Modes), so the hook below sorts calls by command name, and anything it does not recognize as a read asks you.

The simple version: ask before every MifBridge call

If you are fine approving reads too, one rule does it. Add this to .claude/settings.json in your project folder:

{
  "permissions": {
    "ask": ["mcp__mif-bridge"]
  }
}

mcp__mif-bridge matches every tool from the server you registered as mif-bridge. If you registered MifBridge under another name, use that name. Claude Code then shows you every MifBridge call before it runs, reads included. An ask rule still prompts in auto mode.

Do not combine this rule with the hook below. An ask rule always wins over the hook, so reads would ask too.

The full version: reads run, changes ask

Two files in your project folder, the one you open Claude Code in.

1. The hook script, saved as .claude/hooks/mif_ask_before_change.py:

"""Claude Code PreToolUse hook: MifBridge reads run, everything else asks you first.

Reads stdin (the tool call Claude Code is about to make) and prints one decision:
  allow - the call only reads (by the rules below), so no prompt
  ask   - anything else: Claude Code shows you the call and waits for yes or no

It never blocks a call outright. Anything it cannot recognize is treated as a change and asks.
"""
import json
import re
import sys

# Tool names that only read. Names that start with these words read in MifBridge 1.1 and 1.2.
READ_NAME = re.compile(r"^(bl_)?(list|get|describe|find|read)_")
# Read-only tools whose names do not start with one of those words.
READ_TOOLS = {"find_tools", "mif_help", "mif_list_packs", "mif_enable_pack", "self_audit", "bl_status"}
# A read tool given one of these switches can change something (get_level_blueprint's create=true makes one).
WRITE_SWITCHES = {"create", "apply", "confirm", "preview_id", "previewId"}


def decide(call):
    tool = str(call.get("tool_name") or "")
    args = call.get("tool_input") or {}
    name = tool.split("__")[-1] if tool.startswith("mcp__") else tool
    if name == "mif_call":                      # one tool that calls any other by name
        name = str(args.get("tool") or "").strip()
        args = args.get("args") or {}
        if isinstance(args, str):
            try:
                args = json.loads(args)
            except ValueError:
                return "ask", "MifBridge: %s with arguments this hook cannot read" % name
    if not isinstance(args, dict):
        return "ask", "MifBridge: %s with arguments this hook cannot read" % name
    switched = sorted(k for k in WRITE_SWITCHES if args.get(k) not in (None, False, "", 0))
    if (name in READ_TOOLS or READ_NAME.match(name)) and not switched:
        return "allow", "MifBridge read: %s" % name
    return "ask", "MifBridge change: %s %s" % (name, json.dumps(args)[:300])


def main():
    try:
        decision, reason = decide(json.loads(sys.stdin.buffer.read().decode("utf-8-sig")))
    except Exception as exc:  # anything unexpected asks; it never lets a call through unseen
        decision, reason = "ask", "MifBridge: the ask-before-change hook could not read this call (%s)" % exc
    print(json.dumps({"hookSpecificOutput": {"hookEventName": "PreToolUse",
                                             "permissionDecision": decision,
                                             "permissionDecisionReason": reason}}))


if __name__ == "__main__":
    main()

2. The settings, in .claude/settings.json (merge the hooks block into the file if you already have one):

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "mcp__mif-bridge__.*",
        "hooks": [
          {
            "type": "command",
            "command": "python",
            "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/mif_ask_before_change.py"]
          }
        ]
      }
    ]
  }
}
  • matcher picks which tool calls run the hook. mcp__mif-bridge__.* is every MifBridge tool. If you registered MifBridge under another name, change mif-bridge here; the script itself works with any name.
  • command is the Python that runs the script. Use the same one your MifBridge MCP setup starts. If that is a full path to a python.exe, put the same path here.
  • args runs the script directly, with no shell in between, so the path works the same on Windows, macOS and Linux. ${CLAUDE_PROJECT_DIR} is the folder Claude Code was opened in.
  • Restart Claude Code after saving, so it loads the hook.

What counts as a read

  • Any command whose name starts with list_, get_, describe_, find_ or read_, and the same with bl_ in front for Blender. For example list_level_actors, get_property, describe_endpoint and bl_list_objects.
  • Six tools that only look things up: find_tools, mif_help, mif_list_packs, mif_enable_pack, self_audit and bl_status. (mif_enable_pack adds tools to the assistant's session and changes nothing in your project.)
  • Not when the call passes create, apply, confirm or a preview id. get_level_blueprint with create set to true makes a Level Blueprint, so it asks.

Everything else asks, including run_python, every save, delete, spawn and property change, and any command the script does not recognize. To let another read-only command run without asking, add its name to READ_TOOLS.

Where the files can live

File Applies to Shared with your team
.claude/settings.json in the project This project Yes, if you commit it
.claude/settings.local.json in the project This project No, it stays on your machine
~/.claude/settings.json Every project you open No

For the user-wide file, put the script somewhere fixed and give its full path in args, because ${CLAUDE_PROJECT_DIR} changes with each project.

Check that it works

Run the script by hand with a sample call. In PowerShell, from the project folder:

'{"tool_name": "mcp__mif-bridge__mif_call", "tool_input": {"tool": "delete_level_actor", "args": {"confirm": true}}}' | python .claude\hooks\mif_ask_before_change.py
'{"tool_name": "mcp__mif-bridge__mif_call", "tool_input": {"tool": "list_level_actors"}}' | python .claude\hooks\mif_ask_before_change.py

The first prints "permissionDecision": "ask", the second "permissionDecision": "allow".

Then, in Claude Code, ask the assistant to list the actors in the level (it should answer without a prompt) and then to move one (you should get a prompt that names the command).

What was tested for this page: the script on 15 sample calls (reads, Blender reads, writes, deletes, a read with create set to true, run_python, arguments sent as text, and unreadable input), and Claude Code itself with the hook against a stand-in MifBridge server, in Manual mode and in auto mode. In both modes the reads ran and the change was held back; with no hook, nothing ran without approval.

Limits

  • It sorts by name. Every MifBridge command whose name starts with one of the read words was checked, by its description and parameters, to only read, apart from the switches above. A command added in a later version is sorted the same way, so check what a new command does before you add it to READ_TOOLS.
  • It covers MifBridge calls only. Claude Code's own file edits and shell commands follow your normal Claude Code permission settings, and a shell command or file edit can change project files too.
  • Permission modes. In Manual mode and auto mode, the hook's prompt appears. In bypassPermissions mode, Claude Code documents that explicit ask and deny rules still hold but does not promise the same for a hook's prompt, so do not rely on the hook there; use the simple version's ask rule instead. With claude -p (no one at the keyboard), a call that would ask is refused.
  • A broken hook does not ask. If the hook cannot start (for example, the command names a Python that is not there), Claude Code treats that as a hook error and carries on with its normal permission flow. In Manual mode that still prompts you for MifBridge tools; in auto mode Claude Code's own safety check decides. After setting it up, run the check above once.
  • Answer with a plain yes. If a prompt offers to stop asking about the tool, that choice is about mif_call as a whole, writes included, not about the one command. Answer each change with a plain yes or no.

MifBridge's own safeguards

The hook decides whether a call is sent. MifBridge also checks calls once they arrive:

  • Write mode. In the Unreal plugin's default mode, scratch, saves, Play-In-Editor, cooks and console commands are refused, and no agent can change the mode. See Safety and Write Modes.
  • Confirm for destructive changes. Deleting or renaming assets, actors, Blueprint members and DataTable rows, among others, needs confirm set to true.
  • Undo. Edits in Unreal can be undone with Ctrl+Z, and the Blender add-on records an undo step after each call that can change the scene.
  • Previews. In 1.1, bulk writes such as batch, spawn_many and write_datatable_rows can be previewed first: the preview writes nothing and returns an id, and only a second call with that id applies exactly what was previewed.
  • The change cap, from 1.2. Any single call that would change more than 25 things (actors, assets, objects or rows) writes nothing and returns the full list instead. It is applied only when that exact list is committed. Each project can set its own limit with mif_project_mode.

Other assistants

This page covers Claude Code. Cursor, Claude Desktop, Codex and other MCP clients have their own tool-approval settings; see each one's documentation. The same problem applies there: if the client approves by tool name, one approval for mif_call covers reads and writes alike.

Sources

Checked on October 2, 2026.