{"id":43857103,"url":"https://github.com/dgerlanc/mmi","last_synced_at":"2026-04-02T17:04:35.177Z","repository":{"id":333922842,"uuid":"1132367748","full_name":"dgerlanc/mmi","owner":"dgerlanc","description":"Mother May I - Automating permissions for Claude Code Bash tool calls","archived":false,"fork":false,"pushed_at":"2026-03-24T14:06:49.000Z","size":627,"stargazers_count":12,"open_issues_count":5,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-25T17:51:06.568Z","etag":null,"topics":["claude-code","llms"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dgerlanc.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-01-11T20:38:04.000Z","updated_at":"2026-03-25T04:44:54.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/dgerlanc/mmi","commit_stats":null,"previous_names":["dgerlanc/mmi"],"tags_count":16,"template":false,"template_full_name":null,"purl":"pkg:github/dgerlanc/mmi","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dgerlanc%2Fmmi","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dgerlanc%2Fmmi/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dgerlanc%2Fmmi/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dgerlanc%2Fmmi/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dgerlanc","download_url":"https://codeload.github.com/dgerlanc/mmi/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dgerlanc%2Fmmi/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31311062,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-02T12:59:32.332Z","status":"ssl_error","status_checked_at":"2026-04-02T12:54:48.875Z","response_time":89,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["claude-code","llms"],"created_at":"2026-02-06T09:28:24.506Z","updated_at":"2026-04-02T17:04:35.167Z","avatar_url":"https://github.com/dgerlanc.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cimg src=\"logo.png\" alt=\"MMI Logo\" height=\"250\"\u003e\n\n# mmi (Mother May I?)\n\nA CLI utility that acts as a PreToolUse Hook for Claude Code, providing intelligent auto-approval of safe Bash commands.\n\n## Overview\n\nMMI 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.\n\n**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.\n\n**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.\n\nThe name \"Mother May I?\" references the childhood game where permission must be granted before taking action.\n\nThis project was inspired by this [post](https://matthewrocklin.com/ai-zealotry/#appendix-permissions-file) by Matt Rocklin.\n\n## Installation\n\n### Homebrew (macOS and Linux)\n\n```bash\nbrew install dgerlanc/tap/mmi\n```\n\n### From Source\n\n``` bash\njust install\n```\n\nOR\n\n```bash\ngo build -o mmi\nmv mmi /usr/local/bin/\n```\n\n### Binary Downloads\n\nPre-built binaries for Linux, macOS, and Windows are available on the [Releases](https://github.com/dgerlanc/mmi/releases) page.\n\n## Quick Start\n\n1. Install `mmi` (see above)\n2. Run `mmi init` to create the configuration and set up the Claude Code hook\n3. (Optional) Include an example config for your language stack (see [Example Configurations](#example-configurations))\n\nThe `mmi init` command automatically:\n- Creates a default configuration file at `~/.config/mmi/config.toml`\n- Configures Claude Code's `~/.claude/settings.json` to use mmi as a PreToolUse hook\n\n## Configuration\n\n### Claude Code Hook Setup\n\nRunning `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:\n\n```json\n{\n  \"hooks\": {\n    \"PreToolUse\": [\n      {\n        \"matcher\": \"Bash\",\n        \"hooks\": [\n          {\n            \"type\": \"command\",\n            \"command\": \"mmi\"\n          }\n        ]\n      }\n    ]\n  }\n}\n```\n\n### `mmi` Configuration File\n\n`mmi` uses a TOML configuration file at `~/.config/mmi/config.toml`. Generate the default config:\n\n```bash\nmmi init\n```\n\nOr set a custom location via the `MMI_CONFIG` environment variable.\n\n### Configuration Structure\n\nThe config file has four main sections:\n\n```toml\n# Deny list - patterns always rejected (checked first)\n[[deny.simple]]\nname = \"privilege escalation\"\ncommands = [\"sudo\", \"su\", \"doas\"]\n\n[[deny.regex]]\npattern = 'rm\\s+(-[rRfF]+\\s+)*/'\nname = \"rm root\"\n\n# Wrappers - prefixes stripped before checking core command\n[[wrappers.simple]]\nname = \"env\"\ncommands = [\"env\", \"do\"]\n\n[[wrappers.command]]\ncommand = \"timeout\"\nflags = [\"\u003carg\u003e\"]\n\n# Commands - safe commands allowed to execute\n[[commands.simple]]\nname = \"read-only\"\ncommands = [\"ls\", \"cat\", \"grep\"]\n\n[[commands.subcommand]]\ncommand = \"git\"\nsubcommands = [\"diff\", \"log\", \"status\", \"add\"]\nflags = [\"-C \u003carg\u003e\"]\n\n[[commands.regex]]\npattern = '^(true|false|exit(\\s+\\d+)?)$'\nname = \"shell builtin\"\n\n# Rewrites - reject and suggest corrected alternatives\n[[rewrites.simple]]\nname = \"use uv for python\"\nmatch = [\"python\", \"python3\"]\nreplace = \"uv run python\"\n\n[[rewrites.regex]]\nname = \"use uv for pip\"\npattern = '^pip3?\\b'\nreplace = \"uv pip\"\n```\n\n### Config Includes\n\nSplit your configuration across multiple files:\n\n```toml\ninclude = [\"python.toml\", \"rust.toml\"]\n```\n\nTo use different configurations for different projects, set the `MMI_CONFIG` environment variable to point to a different config directory.\n\n## CLI Commands\n\n### `mmi` (default)\n\nRun as a hook - reads JSON from stdin, outputs approval JSON to stdout.\n\n### `mmi init`\n\nCreate the configuration file and set up the Claude Code hook:\n\n```bash\nmmi init              # Create config (if needed) and configure Claude Code\nmmi init --force      # Overwrite existing config and configure Claude Code\nmmi init --config-only  # Only create config.toml, skip Claude settings\nmmi init --claude-settings /path/to/settings.json  # Use custom settings path\n```\n\n**Behavior:**\n- If the config file doesn't exist or `--force` is used, it creates/overwrites `~/.config/mmi/config.toml`\n- If the config file exists and `--force` is not set, it prints a notice but continues\n- Unless `--config-only` is set, it always configures Claude Code's settings.json (if not already configured)\n\nThis allows you to reconfigure Claude Code hooks without needing to use `--force`, which would unnecessarily overwrite your config file.\n\nThe default config includes basic Unix utilities and shell builtins. For language-specific commands (Python, Node.js, Rust), copy an example config from `examples/`.\n\n### `mmi validate`\n\nValidate configuration and display compiled patterns:\n\n```bash\nmmi validate\n```\n\n### `mmi completion`\n\nGenerate shell completion scripts:\n\n```bash\n# Bash\nmmi completion bash \u003e /etc/bash_completion.d/mmi\n\n# Zsh\nmmi completion zsh \u003e \"${fpath[1]}/_mmi\"\n\n# Fish\nmmi completion fish \u003e ~/.config/fish/completions/mmi.fish\n\n# PowerShell\nmmi completion powershell \u003e mmi.ps1\n```\n\n## Global Flags\n\n| Flag | Description |\n|------|-------------|\n| `-v, --verbose` | Enable debug logging |\n| `--dry-run` | Test command approval without JSON output |\n| `--no-audit-log` | Disable audit logging |\n\n## How It Works\n\n`mmi` uses a four-layer approval model:\n\n1. **Deny List** - Patterns that are always rejected (checked first)\n2. **Wrappers** - Safe command prefixes that can wrap any approved command\n3. **Safe Commands** - Allowlisted commands that are safe to execute\n4. **Rewrites** - Patterns that reject the command and suggest a corrected alternative\n\nWhen a command is submitted, `mmi`:\n\n1. Parses and splits command chains (handling `\u0026\u0026`, `||`, `|`, `;`, `\u0026`)\n   - Unparseable commands (incomplete syntax, unclosed quotes) are rejected\n2. For each segment:\n   - Checks for dangerous patterns (command substitution `$()` or backticks)\n   - Checks deny list\n   - Strips safe wrappers\n   - Checks deny list again on core command\n   - Checks rewrite rules (fires regardless of safe list match)\n   - Checks if core command matches safe patterns\n3. Approves only if ALL segments pass all checks and no rewrites match\n4. Logs all segments to audit trail (all segments are evaluated even if earlier ones fail)\n\n## Default Approved Commands\n\nThe default configuration is intentionally restrictive. Use example configs for language-specific setups.\n\n### Deny List (Always Rejected)\n\n- Privilege escalation: `sudo`, `su`, `doas`\n- Dangerous patterns: `rm -rf /`, `chmod 777`, `dd of=/dev/`, `mkfs.*`\n\n### Safe Wrappers\n\n- `timeout N` - timeout wrapper\n- `nice` / `nice -n N` - process priority\n- `env` - environment setup\n- `VAR=value` - environment variable assignments\n- `do` - loop body prefix\n\n### Safe Commands (Default Config)\n\n| Category | Commands |\n|----------|----------|\n| **Unix Utilities** | `ls`, `cat`, `head`, `tail`, `wc`, `find`, `grep`, `rg`, `file`, `which`, `pwd`, `du`, `df`, `curl`, `sort`, `uniq`, `cut`, `tr`, `awk`, `sed`, `xargs` |\n| **File Ops** | `touch`, `make` |\n| **Shell** | `echo`, `cd`, `true`, `false`, `exit`, `sleep` |\n\n### Additional Commands (via Example Configs)\n\nCopy from `examples/` to enable language-specific commands:\n\n| Config | Enables |\n|--------|---------|\n| `python.toml` | `pytest`, `python`, `ruff`, `uv`, `uvx`, `mypy`, `black`, `isort`, `pip`, git subcommands |\n| `node.toml` | `npm`, `npx`, `node`, `yarn`, `pnpm`, `bun`, `eslint`, `prettier`, `tsc`, git subcommands |\n| `rust.toml` | `cargo`, `rustup`, `maturin`, `rustc`, `rustfmt`, git subcommands |\n| `minimal.toml` | Basic read-only commands plus git read-only (`status`, `log`, `diff`, `show`, `branch`) |\n| `strict.toml` | Read-only only, denies file modifications |\n\n## Audit Logging\n\n`mmi` logs all approval decisions to `~/.local/share/mmi/audit.log` in JSON-lines format. Disable with `--no-audit-log`.\n\n\u003cdetails\u003e\n\u003csummary\u003eExample audit log entries\u003c/summary\u003e\n\n**Approved command:**\n```json\n{\n  \"version\": 1,\n  \"tool_use_id\": \"toolu_abc123\",\n  \"session_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n  \"timestamp\": \"2026-01-15T10:30:00.5Z\",\n  \"duration_ms\": 0.42,\n  \"command\": \"git status\",\n  \"approved\": true,\n  \"segments\": [\n    {\n      \"command\": \"git status\",\n      \"approved\": true,\n      \"match\": {\n        \"type\": \"subcommand\",\n        \"name\": \"git\"\n      }\n    }\n  ],\n  \"cwd\": \"/home/user/project\"\n}\n```\n\n**Rejected command (deny match):**\n```json\n{\n  \"version\": 1,\n  \"tool_use_id\": \"toolu_def456\",\n  \"session_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n  \"timestamp\": \"2026-01-15T10:30:05.1Z\",\n  \"duration_ms\": 0.38,\n  \"command\": \"rm -rf /\",\n  \"approved\": false,\n  \"segments\": [\n    {\n      \"command\": \"rm -rf /\",\n      \"approved\": false,\n      \"rejection\": {\n        \"code\": \"DENY_MATCH\",\n        \"name\": \"rm root\",\n        \"pattern\": \"rm\\\\s+(-[rRfF]+\\\\s+)*/\"\n      }\n    }\n  ],\n  \"cwd\": \"/home/user/project\"\n}\n```\n\n**Rewritten command:**\n```json\n{\n  \"version\": 1,\n  \"tool_use_id\": \"toolu_ghi789\",\n  \"session_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n  \"timestamp\": \"2026-01-15T10:30:10.2Z\",\n  \"duration_ms\": 0.35,\n  \"command\": \"python3 script.py\",\n  \"approved\": false,\n  \"segments\": [\n    {\n      \"command\": \"python3 script.py\",\n      \"approved\": false,\n      \"rejection\": {\n        \"code\": \"REWRITE\",\n        \"name\": \"use uv for python\",\n        \"pattern\": \"^python3\\\\b\",\n        \"detail\": \"uv run python script.py\"\n      }\n    }\n  ],\n  \"cwd\": \"/home/user/project\"\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eAudit log field reference\u003c/summary\u003e\n\n| Field | Description |\n|-------|-------------|\n| `version` | Log format version (currently 1) |\n| `tool_use_id` | Claude Code tool use identifier |\n| `session_id` | Claude Code session identifier |\n| `timestamp` | UTC timestamp with tenths of second precision |\n| `duration_ms` | Processing time in milliseconds |\n| `command` | The full command that was evaluated |\n| `approved` | Whether the command was approved |\n| `segments` | Array of individual command segments (for chained commands) |\n| `cwd` | Working directory |\n\n**Segment fields:**\n| Field | Description |\n|-------|-------------|\n| `match` | Present when approved; contains `type`, `pattern`, and `name` |\n| `rejection` | Present when rejected; contains `code` and optionally `name`, `pattern`, `detail` |\n\n\u003c/details\u003e\n\n## Command Rewrites\n\nRewrites 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.\n\nRewrites 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.\n\n### Simple Rewrites\n\nMatch command names and replace the prefix, preserving arguments:\n\n```toml\n[[rewrites.simple]]\nname = \"use uv for python\"\nmatch = [\"python\", \"python3\"]\nreplace = \"uv run python\"\n```\n\n`python3 script.py --verbose` → suggests `uv run python script.py --verbose`\n\n### Regex Rewrites\n\nMatch a pattern with capture group support via `Regexp.ReplaceAllString`:\n\n```toml\n[[rewrites.regex]]\nname = \"use uv for pip\"\npattern = '^pip3?\\b'\nreplace = \"uv pip\"\n```\n\n`pip3 install requests` → suggests `uv pip install requests`\n\nRewrite rules from included config files are merged by appending. The first matching rule wins.\n\n## Security Model\n\n`mmi` follows a **fail-secure default**:\n\n- Deny patterns are checked first and override all approvals (including rewrites)\n- Unrecognized commands are automatically rejected\n- Unparseable commands (incomplete syntax, unclosed quotes) are rejected\n- Command substitution (`$(...)` and backticks) is always rejected (except in quoted heredocs)\n- Command chains are only approved if ALL segments are safe and no rewrites match\n- All segments are evaluated and logged even if earlier segments fail\n- Only explicitly allowlisted patterns are allowed\n- Rewrite suggestions are hints, not bypasses — the rewritten command goes through the full approval pipeline from scratch\n- Shell loops (`while`, `for`) must be complete; their inner commands are extracted and validated individually\n\n## Example Configurations\n\nThe `examples/` directory contains ready-to-use configurations:\n\n- `minimal.toml` - Bare-bones for security-conscious users\n- `python.toml` - Python development (pytest, uv, ruff, mypy, etc.)\n- `node.toml` - Node.js development (npm, yarn, pnpm, bun, etc.)\n- `rust.toml` - Rust development (cargo, rustup, maturin, etc.)\n- `strict.toml` - Read-only commands only\n\nTo use an example config:\n\n```bash\n# Replace default config with an example\ncp examples/python.toml ~/.config/mmi/config.toml\n\n# Or use includes to combine configs\necho 'include = [\"python.toml\"]' \u003e\u003e ~/.config/mmi/config.toml\ncp examples/python.toml ~/.config/mmi/\n```\n\n## Output Format\n\n`mmi` outputs JSON decisions:\n\n```json\n{\n  \"hookSpecificOutput\": {\n    \"hookEventName\": \"PreToolUse\",\n    \"permissionDecision\": \"allow\",\n    \"permissionDecisionReason\": \"timeout + pytest\"\n  }\n}\n```\n\n## FAQ\n\n### What happens if I don't run `mmi init` first?\n\nIf 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.\n\n### Why are command substitutions (`$(...)` and backticks) always rejected?\n\nCommand 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.\n\n**Exception**: Content inside quoted heredocs (single or double quoted delimiters) is treated as literal text and won't trigger rejection:\n```bash\ncat \u003e file.go \u003c\u003c 'EOF'\nfmt.Printf(`template`)  # Allowed - quoted heredoc\nEOF\n```\n\n### How do I test if a command will be approved?\n\nUse `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.\n\n### Can I have different configurations for different projects?\n\nYes, 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.\n\n### How do wrappers work?\n\nWrappers 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.\n\n### Where are approval decisions logged?\n\nAudit 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`.\n\n### Why is my command rejected even though I added it to my config?\n\nCommon causes:\n- **Deny list priority**: Deny patterns are checked first and override all approvals\n- **Rewrite rules**: A rewrite rule may match the command even if it's safe-listed — rewrites take priority over safe matches\n- **Command substitution**: Commands containing `$(...)` or backticks are rejected (except in quoted heredocs)\n- **Command chains**: If using `\u0026\u0026`, `||`, `|`, or `;`, all segments must be approved\n- **Pattern mismatch**: Use `mmi validate` to verify your patterns and `--verbose` to see why rejection occurred\n\n### Can I reconfigure Claude Code hooks without overwriting my config?\n\nYes! 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.\n\n## Testing\n\nRun the test suite:\n\n```bash\ngo test -v ./...\n```\n\nRun with coverage:\n\n```bash\ngo test -coverprofile=coverage.out ./...\ngo tool cover -html=coverage.out\n```\n\n## Creating Releases\n\nReleases are automated via GitHub Actions and GoReleaser.\n\n### Steps\n\n1. **Update the changelog** - Move items from `[Unreleased]` to a versioned section in `CHANGELOG.md`:\n   ```markdown\n   ## [0.1.0] - 2026-01-13\n   ```\n\n2. **Create and push a git tag**:\n   ```bash\n   git tag v0.1.0\n   git push origin v0.1.0\n   ```\n\n3. **Automated release** - GitHub Actions will automatically:\n   - Build binaries for Linux, macOS, Windows (amd64 + arm64)\n   - Create the GitHub release with archives and checksums\n   - Update the Homebrew tap (`dgerlanc/homebrew-tap`)\n\n### Test Locally First (Optional)\n\n```bash\njust release-test\n```\n\nThis validates the GoReleaser config and performs a dry-run snapshot build.\n\n## License\n\nSee LICENSE file for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdgerlanc%2Fmmi","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdgerlanc%2Fmmi","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdgerlanc%2Fmmi/lists"}