mmi
Health Gecti
- License — License: Apache-2.0
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Community trust — 14 GitHub stars
Code Gecti
- Code scan — Scanned 10 files during light audit, no dangerous patterns found
Permissions Gecti
- Permissions — No dangerous permissions requested
Bu listing icin henuz AI raporu yok.
Mother May I - Automating permissions for Claude Code Bash tool calls
mmi (Mother May I?)
A CLI utility that acts as a PreToolUse Hook for Claude Code, providing intelligent auto-approval of safe Bash commands.
Overview
MMI parses Bash commands and automatically approves those that the user specifies as safe, eliminating the need for manual approval on every command. This significantly speeds up development workflows while maintaining security through a configurable deny/allowlist approach.
Important: Allowing an LLM to execute arbitrary Bash commands in a non-sandboxed environment is inherently unsafe. MMI may reduce that risk but cannot guarantee safety! Use at your own risk and always review your configuration carefully.
Note: Claude Code now offers a built-in Bash sandbox mode that restricts file system and network access. You can enable it in your Claude Code settings. MMI can be used alongside sandbox mode for additional control over which commands are auto-approved.
The name "Mother May I?" references the childhood game where permission must be granted before taking action.
This project was inspired by this post by Matt Rocklin.
Installation
Homebrew (macOS and Linux)
brew install dgerlanc/tap/mmi
From Source
just install
OR
go build -o mmi
mv mmi /usr/local/bin/
Binary Downloads
Pre-built binaries for Linux, macOS, and Windows are available on the Releases page.
Quick Start
- Install
mmi(see above) - Run
mmi initto create the configuration and set up the Claude Code hook - (Optional) Include an example config for your language stack (see Example Configurations)
The mmi init command automatically:
- Creates a default configuration file at
~/.config/mmi/config.toml - Configures Claude Code's
~/.claude/settings.jsonto use mmi as a PreToolUse hook
Configuration
Claude Code Hook Setup
Running mmi init automatically configures Claude Code's ~/.claude/settings.json with the mmi hook. If you need to configure it manually, add this to your settings:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "mmi"
}
]
}
]
}
}
mmi Configuration File
mmi uses a TOML configuration file at ~/.config/mmi/config.toml. Generate the default config:
mmi init
Or set a custom location via the MMI_CONFIG environment variable.
Configuration Structure
The config file has four main sections:
# Deny list - patterns always rejected (checked first)
[[deny.simple]]
name = "privilege escalation"
commands = ["sudo", "su", "doas"]
[[deny.regex]]
pattern = 'rm\s+(-[rRfF]+\s+)*/'
name = "rm root"
# Wrappers - prefixes stripped before checking core command
[[wrappers.simple]]
name = "env"
commands = ["env", "do"]
[[wrappers.command]]
command = "timeout"
flags = ["<arg>"]
# Commands - safe commands allowed to execute
[[commands.simple]]
name = "read-only"
commands = ["ls", "cat", "grep"]
[[commands.subcommand]]
command = "git"
subcommands = ["diff", "log", "status", "add"]
flags = ["-C <arg>"]
[[commands.regex]]
pattern = '^(true|false|exit(\s+\d+)?)$'
name = "shell builtin"
# Rewrites - reject and suggest corrected alternatives
[[rewrites.simple]]
name = "use uv for python"
match = ["python", "python3"]
replace = "uv run python"
[[rewrites.regex]]
name = "use uv for pip"
pattern = '^pip3?\b'
replace = "uv pip"
Config Includes
Split your configuration across multiple files:
include = ["python.toml", "rust.toml"]
To use different configurations for different projects, set the MMI_CONFIG environment variable to point to a different config directory.
CLI Commands
mmi (default)
Run as a hook - reads JSON from stdin, outputs approval JSON to stdout.
mmi init
Create the configuration file and set up the Claude Code hook:
mmi init # Create config (if needed) and configure Claude Code
mmi init --force # Overwrite existing config and configure Claude Code
mmi init --config-only # Only create config.toml, skip Claude settings
mmi init --claude-settings /path/to/settings.json # Use custom settings path
Behavior:
- If the config file doesn't exist or
--forceis used, it creates/overwrites~/.config/mmi/config.toml - If the config file exists and
--forceis not set, it prints a notice but continues - Unless
--config-onlyis set, it always configures Claude Code's settings.json (if not already configured)
This allows you to reconfigure Claude Code hooks without needing to use --force, which would unnecessarily overwrite your config file.
The default config includes basic Unix utilities and shell builtins. For language-specific commands (Python, Node.js, Rust), copy an example config from examples/.
mmi validate
Validate configuration and display compiled patterns:
mmi validate
mmi completion
Generate shell completion scripts:
# Bash
mmi completion bash > /etc/bash_completion.d/mmi
# Zsh
mmi completion zsh > "${fpath[1]}/_mmi"
# Fish
mmi completion fish > ~/.config/fish/completions/mmi.fish
# PowerShell
mmi completion powershell > mmi.ps1
Global Flags
| Flag | Description |
|---|---|
-v, --verbose |
Enable debug logging |
--dry-run |
Test command approval without JSON output |
--no-audit-log |
Disable audit logging |
How It Works
mmi uses a four-layer approval model:
- Deny List - Patterns that are always rejected (checked first)
- Wrappers - Safe command prefixes that can wrap any approved command
- Safe Commands - Allowlisted commands that are safe to execute
- Rewrites - Patterns that reject the command and suggest a corrected alternative
When a command is submitted, mmi:
- Parses and splits command chains (handling
&&,||,|,;,&)- Unparseable commands (incomplete syntax, unclosed quotes) are rejected
- For each segment:
- Checks for dangerous patterns (command substitution
$()or backticks) - Checks deny list
- Strips safe wrappers
- Checks deny list again on core command
- Checks rewrite rules (fires regardless of safe list match)
- Checks if core command matches safe patterns
- Checks for dangerous patterns (command substitution
- Approves only if ALL segments pass all checks and no rewrites match
- Logs all segments to audit trail (all segments are evaluated even if earlier ones fail)
Default Approved Commands
The default configuration is intentionally restrictive. Use example configs for language-specific setups.
Deny List (Always Rejected)
- Privilege escalation:
sudo,su,doas - Dangerous patterns:
rm -rf /,chmod 777,dd of=/dev/,mkfs.*
Safe Wrappers
timeout N- timeout wrappernice/nice -n N- process priorityenv- environment setupVAR=value- environment variable assignmentsdo- loop body prefix
Safe Commands (Default Config)
| Category | Commands |
|---|---|
| Unix Utilities | ls, cat, head, tail, wc, find, grep, rg, file, which, pwd, du, df, curl, sort, uniq, cut, tr, awk, sed, xargs |
| File Ops | touch, make |
| Shell | echo, cd, true, false, exit, sleep |
Additional Commands (via Example Configs)
Copy from examples/ to enable language-specific commands:
| Config | Enables |
|---|---|
python.toml |
pytest, python, ruff, uv, uvx, mypy, black, isort, pip, git subcommands |
node.toml |
npm, npx, node, yarn, pnpm, bun, eslint, prettier, tsc, git subcommands |
rust.toml |
cargo, rustup, maturin, rustc, rustfmt, git subcommands |
minimal.toml |
Basic read-only commands plus git read-only (status, log, diff, show, branch) |
strict.toml |
Read-only only, denies file modifications |
Audit Logging
mmi logs all approval decisions to ~/.local/share/mmi/audit.log in JSON-lines format. Disable with --no-audit-log.
Approved command:
{
"version": 1,
"tool_use_id": "toolu_abc123",
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-01-15T10:30:00.5Z",
"duration_ms": 0.42,
"command": "git status",
"approved": true,
"segments": [
{
"command": "git status",
"approved": true,
"match": {
"type": "subcommand",
"name": "git"
}
}
],
"cwd": "/home/user/project"
}
Rejected command (deny match):
{
"version": 1,
"tool_use_id": "toolu_def456",
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-01-15T10:30:05.1Z",
"duration_ms": 0.38,
"command": "rm -rf /",
"approved": false,
"segments": [
{
"command": "rm -rf /",
"approved": false,
"rejection": {
"code": "DENY_MATCH",
"name": "rm root",
"pattern": "rm\\s+(-[rRfF]+\\s+)*/"
}
}
],
"cwd": "/home/user/project"
}
Rewritten command:
{
"version": 1,
"tool_use_id": "toolu_ghi789",
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-01-15T10:30:10.2Z",
"duration_ms": 0.35,
"command": "python3 script.py",
"approved": false,
"segments": [
{
"command": "python3 script.py",
"approved": false,
"rejection": {
"code": "REWRITE",
"name": "use uv for python",
"pattern": "^python3\\b",
"detail": "uv run python script.py"
}
}
],
"cwd": "/home/user/project"
}
Audit log field reference
| Field | Description |
|---|---|
version |
Log format version (currently 1) |
tool_use_id |
Claude Code tool use identifier |
session_id |
Claude Code session identifier |
timestamp |
UTC timestamp with tenths of second precision |
duration_ms |
Processing time in milliseconds |
command |
The full command that was evaluated |
approved |
Whether the command was approved |
segments |
Array of individual command segments (for chained commands) |
cwd |
Working directory |
Segment fields:
| Field | Description |
|---|---|
match |
Present when approved; contains type, pattern, and name |
rejection |
Present when rejected; contains code and optionally name, pattern, detail |
Command Rewrites
Rewrites let you enforce preferred tooling by rejecting commands and suggesting corrected alternatives. When a rewrite rule matches, mmi denies the command with a reason containing the suggested command, prompting Claude to retry with the correct command.
Rewrites are checked after deny/wrapper processing but fire regardless of whether the command is safe-listed. Deny-matched and dangerous-pattern segments are never rewritten. In command chains, each rewritten segment is reported individually.
Simple Rewrites
Match command names and replace the prefix, preserving arguments:
[[rewrites.simple]]
name = "use uv for python"
match = ["python", "python3"]
replace = "uv run python"
python3 script.py --verbose → suggests uv run python script.py --verbose
Regex Rewrites
Match a pattern with capture group support via Regexp.ReplaceAllString:
[[rewrites.regex]]
name = "use uv for pip"
pattern = '^pip3?\b'
replace = "uv pip"
pip3 install requests → suggests uv pip install requests
Rewrite rules from included config files are merged by appending. The first matching rule wins.
Security Model
mmi follows a fail-secure default:
- Deny patterns are checked first and override all approvals (including rewrites)
- Unrecognized commands are automatically rejected
- Unparseable commands (incomplete syntax, unclosed quotes) are rejected
- Command substitution (
$(...)and backticks) is always rejected (except in quoted heredocs) - Command chains are only approved if ALL segments are safe and no rewrites match
- All segments are evaluated and logged even if earlier segments fail
- Only explicitly allowlisted patterns are allowed
- Rewrite suggestions are hints, not bypasses — the rewritten command goes through the full approval pipeline from scratch
- Shell loops (
while,for) must be complete; their inner commands are extracted and validated individually
Example Configurations
The examples/ directory contains ready-to-use configurations:
minimal.toml- Bare-bones for security-conscious userspython.toml- Python development (pytest, uv, ruff, mypy, etc.)node.toml- Node.js development (npm, yarn, pnpm, bun, etc.)rust.toml- Rust development (cargo, rustup, maturin, etc.)strict.toml- Read-only commands only
To use an example config:
# Replace default config with an example
cp examples/python.toml ~/.config/mmi/config.toml
# Or use includes to combine configs
echo 'include = ["python.toml"]' >> ~/.config/mmi/config.toml
cp examples/python.toml ~/.config/mmi/
Output Format
mmi outputs JSON decisions:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "timeout + pytest"
}
}
FAQ
What happens if I don't run mmi init first?
If no configuration file exists at ~/.config/mmi/config.toml (or the path specified by MMI_CONFIG), mmi will reject all commands. This fail-secure behavior ensures that commands are never auto-approved without explicit configuration. Run mmi init to create a default configuration file.
Why are command substitutions ($(...) and backticks) always rejected?
Command substitution can execute arbitrary commands inside what appears to be a safe command. For example, echo $(rm -rf /) looks like an echo command but actually deletes files. mmi rejects both $(...) and backtick syntaxes for security.
Exception: Content inside quoted heredocs (single or double quoted delimiters) is treated as literal text and won't trigger rejection:
cat > file.go << 'EOF'
fmt.Printf(`template`) # Allowed - quoted heredoc
EOF
How do I test if a command will be approved?
Use mmi validate to see your compiled patterns, or use the --dry-run flag to test specific commands without producing JSON output. Add --verbose for detailed debug logs showing why a command was approved or rejected.
Can I have different configurations for different projects?
Yes, use the MMI_CONFIG environment variable to point to a different config directory. For example, set MMI_CONFIG=/path/to/project/.mmi to use a project-specific configuration.
How do wrappers work?
Wrappers are safe prefixes that are stripped before checking the core command. For example, if timeout is a wrapper and pytest is approved, then timeout 10 pytest is approved. Wrappers don't make unsafe commands safe—they simply allow safe commands to be wrapped with approved prefixes.
Where are approval decisions logged?
Audit logs are written to ~/.local/share/mmi/audit.log in JSON-lines format. Each entry includes metadata (version, session/tool IDs, timestamp, duration), the command, approval status, detailed segment information with match or rejection details, and the working directory. Disable logging with --no-audit-log.
Why is my command rejected even though I added it to my config?
Common causes:
- Deny list priority: Deny patterns are checked first and override all approvals
- Rewrite rules: A rewrite rule may match the command even if it's safe-listed — rewrites take priority over safe matches
- Command substitution: Commands containing
$(...)or backticks are rejected (except in quoted heredocs) - Command chains: If using
&&,||,|, or;, all segments must be approved - Pattern mismatch: Use
mmi validateto verify your patterns and--verboseto see why rejection occurred
Can I reconfigure Claude Code hooks without overwriting my config?
Yes! Simply run mmi init again. If your config file already exists, it will print a notice and skip writing the config file, but will still configure Claude Code's settings.json (unless --config-only is set or the hook is already configured). Use --force only if you want to overwrite your config file.
Testing
Run the test suite:
go test -v ./...
Run with coverage:
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
Creating Releases
Releases are automated via GitHub Actions and GoReleaser.
Steps
Update the changelog - Move items from
[Unreleased]to a versioned section inCHANGELOG.md:## [0.1.0] - 2026-01-13Create and push a git tag:
git tag v0.1.0 git push origin v0.1.0Automated release - GitHub Actions will automatically:
- Build binaries for Linux, macOS, Windows (amd64 + arm64)
- Create the GitHub release with archives and checksums
- Update the Homebrew tap (
dgerlanc/homebrew-tap)
Test Locally First (Optional)
just release-test
This validates the GoReleaser config and performs a dry-run snapshot build.
License
See LICENSE file for details.
Yorumlar (0)
Yorum birakmak icin giris yap.
Yorum birakSonuc bulunamadi