Skip to main content
Tool hooks are experimental. Expect breaking changes while we iterate.
Tool hooks let you run your own scripts before and after mux tool executions.

What do you want to do?


Block dangerous commands

Create .mux/tool_pre to validate commands before they run. Exit non-zero to block:
#!/usr/bin/env bash
# .mux/tool_pre - runs before every tool

if [[ "$MUX_TOOL" == "bash" ]]; then
  script=$(echo "$MUX_TOOL_INPUT" | jq -r '.script')

  if echo "$script" | grep -q 'push.*--force'; then
    echo "❌ Force push blocked" >&2
    exit 1
  fi

  if echo "$script" | grep -q 'rm -rf /'; then
    echo "❌ Dangerous rm blocked" >&2
    exit 1
  fi
fi

exit 0  # Allow tool to run
chmod +x .mux/tool_pre
The agent sees your error message and can adjust its approach.

Lint after file edits

Create .mux/tool_post to run validation after tools complete:
#!/usr/bin/env bash
# .mux/tool_post - runs after every tool

if [[ "$MUX_TOOL" == file_edit_* ]]; then
  file=$(echo "$MUX_TOOL_INPUT" | jq -r '.file_path')

  case "$file" in
    *.py)
      ruff check "$file" 2>&1 || exit 1
      ;;
    *.ts|*.tsx)
      npx tsc --noEmit "$file" 2>&1 || exit 1
      ;;
  esac
fi
chmod +x .mux/tool_post
Lint errors appear in hook_output and the agent can fix them.

Set up environment

Create .mux/tool_env to configure your shell environment. This file is sourced before every bash tool call:
# .mux/tool_env - sourced before bash commands

# direnv
eval "$(direnv export bash 2>/dev/null)" || true

# nvm
# export NVM_DIR="$HOME/.nvm"
# [ -s "$NVM_DIR/nvm.sh" ] && source "$NVM_DIR/nvm.sh"

# Python virtualenv
# source .venv/bin/activate 2>/dev/null || true
Unlike hooks, tool_env doesn’t need to be executable—it’s sourced, not run. It only affects bash tools.

Reference

All hooks receive these environment variables:
VariableDescription
MUX_TOOLTool name: bash, file_edit_replace_string, file_read, etc.
MUX_TOOL_INPUTJSON string with tool arguments (placeholder if large)
MUX_TOOL_INPUT_PATHPath to full input JSON (when input is large)
MUX_WORKSPACE_IDCurrent workspace identifier
MUX_PROJECT_DIRWorkspace root directory
Post-hook only (tool_post):
VariableDescription
MUX_TOOL_RESULTTool result JSON (truncated if large)
MUX_TOOL_RESULT_PATHPath to full result JSON file
Exit Codetool_pre behaviortool_post behavior
0Tool executes normallySuccess, output shown to agent
Non-zeroTool blocked, error shown to agentFailure, error shown in hook_output
mux searches for each hook file in this order:
  1. Project-level: .mux/<hook>
  2. User-level: ~/.mux/<hook>
This applies to tool_pre, tool_post, and tool_env.For SSH workspaces, hooks execute on the remote machine.
Hooks must complete within 5 minutes or they’re terminated. Long-running tools (builds, tests) don’t count against this—only hook execution time.
Feature.mux/tool_pre.mux/tool_post.mux/tool_env
PurposeBlock dangerous commandsLint/validate resultsEnvironment setup
RunsBefore toolAfter toolSourced in bash shell
Applies toAll toolsAll toolsbash tool only
Use caseBlock force-pushRun ruff/eslintdirenv, nvm, virtualenv

More examples

#!/usr/bin/env bash
# .mux/tool_post

if [[ "$MUX_TOOL" == file_edit_* ]]; then
  file=$(echo "$MUX_TOOL_INPUT" | jq -r '.file_path')

  if prettier --write "$file" 2>/dev/null; then
    echo "Formatted: $file" >&2
  fi
fi
#!/usr/bin/env bash
# .mux/tool_post

# MUX_TOOL_RESULT contains the result JSON
echo "$(date '+%H:%M:%S') $MUX_TOOL completed" >> /tmp/mux-tools.log
#!/usr/bin/env python3
# .mux/tool_pre

import os, sys, json

tool = os.environ.get('MUX_TOOL', '')
tool_input = json.loads(os.environ.get('MUX_TOOL_INPUT', '{}'))

if tool == 'bash':
    script = tool_input.get('script', '')
    if 'rm -rf /' in script:
        print("❌ Blocked dangerous command", file=sys.stderr)
        sys.exit(1)

sys.exit(0)  # Allow