diff --git a/CLAUDE.md b/CLAUDE.md index 0190449..09b5231 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,7 +17,7 @@ These run **when a session begins**: These run **before** a tool call is executed: - `mcp__.*very-good-cli__.*` matcher → `check-vgv-cli.sh` — reads `tool_name` from the payload and exits 0 unless the caller is a Very Good CLI tool, so a host that does not apply the matcher cannot have the decision land on an unrelated tool; for its own tools it auto-approves the call by returning a PreToolUse `allow` decision, so it is always permitted regardless of run mode (interactive, headless, or `skipAutoPermissionPrompt`) and never dead-ends when the tool isn't on `permissions.allow`; denies with an install/upgrade message if the CLI is missing or < 1.5.0, and stands aside when the CLI is present but its version cannot be read (`dart` missing from `PATH`), leaving normal permission handling to apply. The `.*` in the matcher covers both the bare `mcp__very-good-cli__*` server (repo-root `.mcp.json`) and the plugin-namespaced `mcp__plugin__very-good-cli__*` form used when installed from a marketplace -- `Bash` matcher → `block-cli-workarounds.sh` — prevents direct CLI bypass of VGV CLI commands through the host's shell tool (`Bash`, or `Shell` on other hosts); exits 0 when the payload names a non-shell tool, so an unrelated tool carrying a `command` argument is never inspected; when the CLI is present but cannot run because `dart` is missing from `PATH`, the denial says so rather than redirecting to an MCP server that cannot start; every denial also says the whole shell call was refused, so the agent re-runs any command chained with the blocked one instead of assuming it ran; exits 2 on failure (blocking) +- `Bash` matcher → `block-cli-workarounds.sh` — denies `flutter`/`dart` `test`/`create` and `very_good test`/`create`/`packages` in the shell and points to the MCP tool. Quoted text is data unless `eval`, `sh -c` or `$( )` runs it. A heredoc body with a quoted delimiter (`<<'EOF'`) is data too; an unquoted `</dev/null; then echo "jq is required for block-cli-workarounds hook but not found" >&2 @@ -34,54 +33,111 @@ fi # assumes the chained command's side effect happened and carries on without it. WHOLE_CALL_REFUSED="This whole shell call was refused, so none of it ran: run any other commands it chained in a call of their own." -# Deny with an install/upgrade message when the CLI is missing or outdated, with a PATH -# message when it is present but cannot run, and otherwise redirect to the MCP tool. +# Deny with a reason that fits the CLI status. deny_with_cli_check() { - local mcp_hint="$1" - local cli_status + local mcp_hint="$1" matched="$2" + local cli_status reason cli_status=$(check_vgv_cli) case "$cli_status" in not_installed) - deny "Very Good CLI is required but was not found. Install with: dart pub global activate very_good_cli. $WHOLE_CALL_REFUSED" + reason="Very Good CLI is required but was not found. Install with: dart pub global activate very_good_cli." ;; outdated:*) - local version="${cli_status#outdated:}" - deny "Very Good CLI ${version} is too old (requires >= ${MIN_VERSION}). Update with: dart pub global activate very_good_cli. $WHOLE_CALL_REFUSED" + reason="Very Good CLI ${cli_status#outdated:} is too old (requires >= ${MIN_VERSION}). Update with: dart pub global activate very_good_cli." ;; unverifiable) # Redirecting to the MCP tool here would be a dead end: the server starts through # the same very_good shim, which cannot exec dart from this PATH either. - deny "Very Good CLI was found but could not run: dart is not on the PATH available to hooks, so the very_good_cli MCP server cannot start either. Add the Dart SDK bin directory to PATH for non-interactive shells (e.g. in ~/.zprofile) and start a new session. $WHOLE_CALL_REFUSED" + reason="Very Good CLI was found but could not run: dart is not on the PATH available to hooks, so the very_good_cli MCP server cannot start either. Add the Dart SDK bin directory to PATH for non-interactive shells (e.g. in ~/.zprofile) and start a new session." ;; *) - deny "$mcp_hint $WHOLE_CALL_REFUSED" + reason="$mcp_hint" ;; esac + deny "$reason $WHOLE_CALL_REFUSED Matched: $matched" } -# Split on shell operators and check the first two tokens of each subcommand. -# This avoids false positives from file paths (.dart) or quoted strings. -BLOCKED=$(echo "$COMMAND" | awk '{ - n = split($0, parts, /[;&|]+/) +# Quoted text is data unless eval, sh -c or $( ) runs it. Every adjacent word pair is +# checked, so wrappers (fvm, melos, sudo, timeout) and /path/to/flutter need no list. +read -r -d '' find_invocation <<'AWK' || true # read, not $(cat): unbalanced parens inside +BEGIN { + RS = "\001" # whole command is one record + hint["flutter test"] = "Do not use 'flutter test' or 'dart test'. Use the very_good_cli MCP 'test' tool instead." + hint["dart test"] = hint["flutter test"] + hint["flutter create"] = "Do not use 'flutter create' or 'dart create'. Use the very_good_cli MCP 'create' tool instead." + hint["dart create"] = hint["flutter create"] + hint["very_good test"] = "Do not use 'very_good test' via shell. Use the very_good_cli MCP 'test' tool instead." + hint["very_good create"] = "Do not use 'very_good create' via shell. Use the very_good_cli MCP 'create' tool instead." + hint["very_good packages"] = "Do not use 'very_good packages' via shell. Use the very_good_cli MCP 'packages_get' or 'packages_check_licenses' tool instead." +} +# Drop heredoc bodies written with a quoted delimiter (<<'X', <<-"X"). The shell +# expands nothing inside them, so they are data, not commands. An unquoted <