diff --git a/README.md b/README.md index b5ece12..bd33749 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ For more on 1Password's developer tools, see the [1Password Developer Documentat - [Cursor](https://cursor.com) - [sqlite3](https://www.sqlite.org/) installed and available in your `PATH` (pre-installed on macOS; install via your package manager on Linux) -> **Note:** [Local `.env` files](https://developer.1password.com/docs/environments/local-env-file) from 1Password Environments are only available on macOS and Linux. Windows is not yet supported — Cursor will automatically skip validations on Windows. +> **Note:** 1Password Environments local `.env` mounts only apply on **macOS and Linux**. **`hooks.json`** invokes **`./scripts/validate-mounted-env-files`** with no extension. On **macOS / Linux**, that runs the **Bash** script. On **Windows** the shell looks for a real file by trying suffixes from **`PATHEXT`** until one matches on disk. That yields **`validate-mounted-env-files.cmd`**, which returns **`allow`** and skips validation so agent shells are not blocked. ## Installation and Setup @@ -107,7 +107,7 @@ For each file, the hook checks: **Cursor Execution Log** 1. Open **Cursor Settings** > **Hooks** > **Execution Log**. -2. Look for `beforeShellExecution` entries tied to `validate-mounted-env-files.sh`. +2. Look for `beforeShellExecution` entries tied to `validate-mounted-env-files`. 3. Each entry shows the hook's permission decision and any error messages. **Manual Testing with Debug Mode** @@ -115,7 +115,7 @@ For each file, the hook checks: Run the hook directly with `DEBUG=1` to see detailed output on stderr: ```bash -DEBUG=1 echo '{"command": "echo test", "workspace_roots": ["/path/to/your/project"]}' | ./scripts/validate-mounted-env-files.sh +DEBUG=1 echo '{"command": "echo test", "workspace_roots": ["/path/to/your/project"]}' | ./scripts/validate-mounted-env-files ``` **Log File** @@ -133,7 +133,8 @@ When not running in debug mode, the hook writes logs to `/tmp/1password-cursor-h ├── assets/ │ └── logo.svg # Plugin logo ├── scripts/ -│ └── validate-mounted-env-files.sh # Validation hook script +│ ├── validate-mounted-env-files # Bash hook (macOS / Linux) +│ └── validate-mounted-env-files.cmd # Windows cmd wrapper returns allow (validation skipped) ├── LICENSE └── README.md ``` @@ -141,7 +142,7 @@ When not running in debug mode, the hook writes logs to `/tmp/1password-cursor-h ## Resources - [Validate local `.env` files with Cursor Agent](https://developer.1password.com/docs/environments/cursor-hook-validate/) — full setup guide on the 1Password Developer site -- [1Password Cursor Hooks](https://github.com/1Password/cursor-hooks) — the original hooks repository this plugin is based on +- [1Password Agent Hooks](https://github.com/1Password/agent-hooks) — the original hooks repository this plugin is based on - [1Password Environments](https://developer.1password.com/docs/environments) — documentation for 1Password's environment and secrets management - [1Password Local `.env` Files](https://developer.1password.com/docs/environments/local-env-file) — how local `.env` file mounting works - [Cursor Hooks Documentation](https://cursor.com/docs/agent/hooks) — how Cursor hooks work @@ -149,4 +150,4 @@ When not running in debug mode, the hook writes logs to `/tmp/1password-cursor-h ## License -[MIT](./LICENSE) — Copyright (c) 2026 1Password \ No newline at end of file +[MIT](./LICENSE) — Copyright (c) 2026 1Password diff --git a/hooks/hooks.json b/hooks/hooks.json index 4ab9095..d57cfee 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -3,7 +3,7 @@ "hooks": { "beforeShellExecution": [ { - "command": "./scripts/validate-mounted-env-files.sh" + "command": "./scripts/validate-mounted-env-files" } ] } diff --git a/scripts/validate-mounted-env-files.sh b/scripts/validate-mounted-env-files similarity index 96% rename from scripts/validate-mounted-env-files.sh rename to scripts/validate-mounted-env-files index 6dfa9a2..1e0840c 100755 --- a/scripts/validate-mounted-env-files.sh +++ b/scripts/validate-mounted-env-files @@ -8,11 +8,11 @@ set -euo pipefail # # - Global Variables # - Core Utility Functions (logging, JSON escaping) -# - System & Path Utility Functions (OS detection, path normalization) +# - System & Path Utility Functions (uname_platform, path normalization) # - 1Password Database Functions (finding and querying database) # - Mount Parsing & Validation Functions (parsing mount data, validation) # - TOML Parsing Functions -# - Main Execution Logic +# - Main Execution Logic (early platform exit must run before stdin is read) # - Permission Decision Logic # # ============================================================================ @@ -47,7 +47,7 @@ log() { local timestamp timestamp=$(date +"%Y-%m-%d %H:%M:%S" 2>/dev/null || echo "$(date +%s)") local log_message="[${timestamp}] [validate-mounted-env-files] $*" - + if [[ "${DEBUG:-}" == "1" ]]; then # If DEBUG=1, echo directly to terminal (stderr for logs) echo "$log_message" >&2 @@ -77,13 +77,13 @@ escape_json_string() { parse_json_workspace_roots() { local json_input json_input=$(cat) - + # Extract workspace_roots array values # Find the line(s) containing "workspace_roots" and extract the array # Handle both single-line and multi-line arrays local in_array=false local array_lines="" - + while IFS= read -r line || [[ -n "$line" ]]; do # Check if this line starts the workspace_roots array if echo "$line" | grep -qE '"workspace_roots"[[:space:]]*:[[:space:]]*\['; then @@ -105,7 +105,7 @@ parse_json_workspace_roots() { fi fi done <<< "$json_input" - + # Extract quoted strings from the array content echo "$array_lines" | grep -oE '"[^"]+"' | \ sed 's/^"//;s/"$//' | \ @@ -139,17 +139,22 @@ EOF # SYSTEM & PATH UTILITY FUNCTIONS # ============================================================================ -# Detect operating system -detect_os() { - case "$(uname -s)" in +# Classify kernel for DB paths and early platform skip. +uname_platform() { + case "$(uname -s 2>/dev/null || echo "")" in Darwin*) echo "macos" ;; Linux*) echo "unix" ;; + MINGW* | MSYS* | CYGWIN*) + echo "windows_bash" + ;; + FreeBSD*) + echo "freebsd" + ;; *) - log "Warning: Unsupported OS: $(uname -s)" echo "unknown" ;; esac @@ -159,26 +164,26 @@ detect_os() { # Returns 0 if path is safe, 1 if unsafe validate_path() { local path="$1" - + # Check for empty path [[ -z "$path" ]] && return 1 - + # Check for command substitution patterns # $() command substitution if [[ "$path" =~ \$\( ]] || [[ "$path" =~ \$\{ ]]; then return 1 fi - + # Backtick command substitution if [[ "$path" =~ \` ]]; then return 1 fi - + # Check for semicolons, pipes, ampersands, and other command separators if [[ "$path" =~ [\;\|\&\<\>] ]]; then return 1 fi - + # Check for control characters that could break commands # Remove all printable characters; if anything remains, there are control chars local non_printable @@ -186,7 +191,7 @@ validate_path() { if [[ -n "$non_printable" ]]; then return 1 fi - + # Path is considered safe if it passes all checks return 0 } @@ -195,14 +200,14 @@ validate_path() { normalize_path() { local path="$1" local normalized normalized_dir file_part dir_part - + # Validate path before using it with cd to prevent command injection if ! validate_path "$path"; then log "Warning: Unsafe path detected, skipping normalization: ${path}" echo "$path" return 0 fi - + # Normalize a given path using cd # This resolves . and .. components and symlinks for existing paths if [[ -d "$path" ]]; then @@ -216,7 +221,7 @@ normalize_path() { # For files/FIFOs, resolve the directory part dir_part=$(dirname "$path") file_part=$(basename "$path") - + # Validate dir_part before using with cd if validate_path "$dir_part" && [[ -d "$dir_part" ]]; then normalized_dir=$(cd "$dir_part" && pwd 2>/dev/null) @@ -229,7 +234,7 @@ normalize_path() { # Attempt to normalize non-existent paths (e.g., with .. components) dir_part=$(dirname "$path") file_part=$(basename "$path") - + if validate_path "$dir_part" && [[ -d "$dir_part" ]]; then normalized_dir=$(cd "$dir_part" && pwd 2>/dev/null) if [[ -n "$normalized_dir" ]]; then @@ -238,7 +243,7 @@ normalize_path() { fi fi fi - + # Last resort: return path as-is echo "$path" } @@ -252,7 +257,7 @@ find_1password_db() { local os_type="$1" local home_path="${HOME}" local db_paths=() - + if [[ "$os_type" == "macos" ]]; then db_paths=( "${home_path}/Library/Group Containers/2BUA8C4S2C.com.1password/Library/Application Support/1Password/Data/1Password.sqlite" @@ -264,49 +269,49 @@ find_1password_db() { "${home_path}/.var/app/com.onepassword.OnePassword/config/1Password/1Password.sqlite" ) fi - + for db_path in "${db_paths[@]}"; do if [[ -f "$db_path" ]]; then echo "$db_path" return 0 fi done - + return 1 } # Query 1Password database for mounts query_mounts() { local db_path="$1" - + if ! command -v sqlite3 &> /dev/null; then log "Warning: sqlite3 not found, cannot query 1Password database" return 1 fi - + # Check if database is readable if [[ ! -r "$db_path" ]]; then log "Warning: 1Password database is not readable: ${db_path}" return 1 fi - + # Check if database file exists and is a valid SQLite database if ! sqlite3 "$db_path" "SELECT 1;" &>/dev/null; then log "Warning: 1Password database appears to be invalid or locked: ${db_path}" return 1 fi - + # Query for mount entries # Suppress errors but capture output local result result=$(sqlite3 "$db_path" "SELECT hex(data) FROM objects_associated WHERE key_name LIKE 'dev-environment-mount/%';" 2>/dev/null) local exit_code=$? - + if [[ $exit_code -ne 0 ]]; then log "Warning: Failed to query 1Password database (exit code: $exit_code)" return 1 fi - + # Return result even if empty (empty string is valid - means no mounts) echo "$result" return 0 @@ -320,16 +325,16 @@ query_mounts() { is_project_mount() { local mount_path="$1" local project_path="$2" - + # Normalize paths for comparison local normalized_mount normalized_project - + normalized_mount=$(normalize_path "$mount_path") normalized_project=$(normalize_path "$project_path") - + # Ensure both paths end with / for consistent comparison [[ "$normalized_project" != */ ]] && normalized_project="${normalized_project}/" - + # Check if mount path starts with project path (mount is within project) # Also check original paths in case normalization failed if [[ "$normalized_mount" == "$normalized_project"* ]] || \ @@ -338,7 +343,7 @@ is_project_mount() { [[ "$mount_path" == "$project_path" ]]; then return 0 fi - + return 1 } @@ -347,10 +352,10 @@ hex_to_json() { local hex="$1" # Remove any whitespace/newlines hex=$(echo "$hex" | tr -d '[:space:]') - + # Skip if empty [[ -z "$hex" ]] && return 1 - + # Use printf with escaped hex # Convert hex pairs to \x escaped format local escaped_hex decoded @@ -361,7 +366,7 @@ hex_to_json() { echo "$decoded" return 0 fi - + return 1 } @@ -369,46 +374,46 @@ hex_to_json() { parse_mount() { local hex_data="$1" local json_data - + json_data=$(hex_to_json "$hex_data") - + if [[ -z "$json_data" ]]; then return 1 fi - + # Extract mountPath, isEnabled, environmentName, uuid, and environmentUuid from JSON # Note: This may not handle all JSON edge cases (escaped quotes, etc.) # but should work for typical 1Password mount JSON structures local mount_path is_enabled environment_name uuid environment_uuid - + # Extract mountPath - handle both BSD and GNU sed mount_path=$(echo "$json_data" | grep -oE '"mountPath"[[:space:]]*:[[:space:]]*"[^"]*"' | sed -E 's/.*"mountPath"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/' 2>/dev/null || \ echo "$json_data" | grep -o '"mountPath"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"mountPath"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' 2>/dev/null || echo "") - + # Check for isEnabled: true or false if echo "$json_data" | grep -qE '"isEnabled"[[:space:]]*:[[:space:]]*true'; then is_enabled="true" else is_enabled="false" fi - + # Extract environmentName - handle both BSD and GNU sed environment_name=$(echo "$json_data" | grep -oE '"environmentName"[[:space:]]*:[[:space:]]*"[^"]*"' | sed -E 's/.*"environmentName"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/' 2>/dev/null || \ echo "$json_data" | grep -o '"environmentName"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"environmentName"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' 2>/dev/null || echo "") - + # Extract uuid - handle both BSD and GNU sed uuid=$(echo "$json_data" | grep -oE '"uuid"[[:space:]]*:[[:space:]]*"[^"]*"' | sed -E 's/.*"uuid"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/' 2>/dev/null || \ echo "$json_data" | grep -o '"uuid"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"uuid"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' 2>/dev/null || echo "") - + # Extract environmentUuid - handle both BSD and GNU sed environment_uuid=$(echo "$json_data" | grep -oE '"environmentUuid"[[:space:]]*:[[:space:]]*"[^"]*"' | sed -E 's/.*"environmentUuid"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/' 2>/dev/null || \ echo "$json_data" | grep -o '"environmentUuid"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"environmentUuid"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' 2>/dev/null || echo "") - + if [[ -n "$mount_path" ]]; then echo "$mount_path|$is_enabled|$environment_name|$uuid|$environment_uuid" return 0 fi - + return 1 } @@ -419,12 +424,12 @@ parse_mount() { # Remove comments and trim whitespace from a TOML line normalize_toml_line() { local line="$1" - + # Remove comments (everything after #) line="${line%%#*}" # Trim leading/trailing whitespace line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') - + echo "$line" } @@ -432,30 +437,30 @@ normalize_toml_line() { # Returns 0 if mount_paths field exists, 1 otherwise has_toml_mount_paths_field() { local toml_file="$1" - + # Check for TOML configuration if [[ ! -f "$toml_file" ]]; then return 1 fi - + while IFS= read -r raw_line || [[ -n "$raw_line" ]]; do local line line=$(normalize_toml_line "$raw_line") - + # Skip empty lines [[ -z "$line" ]] && continue - + # If we hit a section header, we're no longer at top level if [[ "$line" =~ ^\[\[.*\]\] ]] || [[ "$line" =~ ^\[.*\] ]]; then break fi - + # Detect 'mount_paths' field at top level if [[ "$line" =~ ^mount_paths[[:space:]]*= ]]; then return 0 fi done < "$toml_file" - + return 1 } @@ -465,11 +470,11 @@ has_toml_mount_paths_field() { # Returns exit code 1 if mount_paths field doesn't exist parse_toml_mount_paths() { local toml_file="$1" - + if [[ ! -f "$toml_file" ]]; then return 1 fi - + # Pure bash TOML parsing for environments entries # Handles formats like: # mount_paths = [".env", "billing.env"] @@ -482,19 +487,19 @@ parse_toml_mount_paths() { local mount_paths="" local array_content="" local found_mount_paths_field=false - + while IFS= read -r raw_line || [[ -n "$raw_line" ]]; do local line line=$(normalize_toml_line "$raw_line") - + # Skip empty lines [[ -z "$line" ]] && continue - + # If we hit a section header, we're no longer at top level if [[ "$line" =~ ^\[\[.*\]\] ]] || [[ "$line" =~ ^\[.*\] ]]; then break fi - + # Check for mount_paths = [...] on a single line if [[ "$line" =~ ^mount_paths[[:space:]]*=[[:space:]]*\[.*\] ]]; then found_mount_paths_field=true @@ -503,7 +508,7 @@ parse_toml_mount_paths() { array_part="${array_part%\]*}" array_content="$array_part" in_mount_paths_array=false # Array is complete on one line - + # Extract quoted strings from the array content while [[ "$array_content" =~ \"([^\"]+)\" ]]; do mount_paths="${mount_paths}${BASH_REMATCH[1]}"$'\n' @@ -550,7 +555,7 @@ parse_toml_mount_paths() { fi fi done < "$toml_file" - + # If mount_paths field was found, return success (even if empty) if [[ "$found_mount_paths_field" == "true" ]]; then # Remove trailing newline and return @@ -562,7 +567,7 @@ parse_toml_mount_paths() { fi return 0 fi - + return 1 } @@ -570,6 +575,20 @@ parse_toml_mount_paths() { # MAIN EXECUTION LOGIC # ============================================================================ +# Unsupported platforms: return allow JSON and exit. +case "$(uname_platform)" in + windows_bash | freebsd) + UNSUPPORTED_PLATFORM_SKIP_MSG='1Password Environments local .env validation is only supported on macOS and Linux. Validation was skipped.' + _skip_msg_esc=$(escape_json_string "$UNSUPPORTED_PLATFORM_SKIP_MSG") + _skip_json=$(printf '%s' "{\"permission\":\"allow\",\"agent_message\":\"${_skip_msg_esc}\",\"user_message\":\"${_skip_msg_esc}\"}") + unset _skip_msg_esc + cat >/dev/null 2>/dev/null || true + printf '%s\n' "$_skip_json" + unset _skip_json + exit 0 + ;; +esac + # Query 1Password database and check mounts log "Checking for local .env files mounted by 1Password..." @@ -597,7 +616,11 @@ fi log "Found ${#workspace_roots_array[@]} workspace root(s) to validate" # Query 1Password database once (shared across all workspace roots) -os_type=$(detect_os) +os_type=$(uname_platform) +if [[ "$os_type" != "macos" && "$os_type" != "unix" ]]; then + log "Warning: Unsupported OS: $(uname -s 2>/dev/null || echo unknown)" + os_type="unknown" +fi db_path="" mount_hex_data="" @@ -611,19 +634,19 @@ fi # Process each workspace root for workspace_root in "${workspace_roots_array[@]}"; do log "Processing workspace root: $workspace_root" - + # Check for TOML configuration at this workspace root toml_file="${workspace_root}/.1password/environments.toml" use_configured_mode=false - + # Check if TOML exists and has mount_paths field if [[ -f "$toml_file" ]]; then log "Found environments.toml at ${toml_file}, checking for mount_paths field..." - + if has_toml_mount_paths_field "$toml_file"; then use_configured_mode=true log "environments.toml has mount_paths field defined - validating specified mounts" - + # Parse and validate TOML mount paths toml_mounts=$(parse_toml_mount_paths "$toml_file") if [[ $? -ne 0 ]]; then @@ -639,38 +662,38 @@ for workspace_root in "${workspace_roots_array[@]}"; do else log "No environments.toml found at ${workspace_root}/.1password/environments.toml, using default mode (checking all mounts)" fi - + # Configured mode: validate only mounts specified in TOML if [[ "$use_configured_mode" == "true" ]]; then log "Validating local .env files specified in environments.toml for workspace ${workspace_root}..." - + # Build an array of unique normalized paths from TOML toml_paths_array=() while IFS= read -r mount_path || [[ -n "$mount_path" ]]; do [[ -z "$mount_path" ]] && continue - + # Validate path from TOML to prevent command injection if ! validate_path "$mount_path"; then log "Warning: Unsafe path detected in environments.toml, skipping: ${mount_path}" continue fi - + # Resolve mount path relative to workspace root if [[ "$mount_path" == /* ]]; then resolved_path="$mount_path" else resolved_path="${workspace_root}/${mount_path}" fi - + # Normalize the path resolved_path=$(normalize_path "$resolved_path") - + # Skip mounts that resolve outside the current workspace root if ! is_project_mount "$resolved_path" "$workspace_root"; then log "Skipping required mount outside workspace root: \"${resolved_path}\" (workspace: \"${workspace_root}\")" continue fi - + # Add to array only if not already present path_exists=false if [[ ${#toml_paths_array[@]} -gt 0 ]]; then @@ -685,21 +708,21 @@ for workspace_root in "${workspace_roots_array[@]}"; do toml_paths_array+=("$resolved_path") fi done <<< "$toml_mounts" - + # Check each TOML-specified mount if [[ ${#toml_paths_array[@]} -gt 0 ]]; then for resolved_path in "${toml_paths_array[@]}"; do log "Checking required local .env file from TOML: \"${resolved_path}\"" - + # First, check if it's in the database and what its status is found_in_db=false is_enabled="false" environment_name="" - + if [[ -n "$mount_hex_data" ]]; then while IFS= read -r hex_line || [[ -n "$hex_line" ]]; do [[ -z "$hex_line" ]] && continue - + mount_info=$(parse_mount "$hex_line") if [[ -n "$mount_info" ]]; then mount_path="${mount_info%%|*}" @@ -707,10 +730,10 @@ for workspace_root in "${workspace_roots_array[@]}"; do mount_is_enabled="${remaining%%|*}" remaining="${remaining#*|}" mount_env_name="${remaining%%|*}" - + # Normalize DB mount path for comparison normalized_db_path=$(normalize_path "$mount_path") - + if [[ "$normalized_db_path" == "$resolved_path" ]]; then found_in_db=true is_enabled="$mount_is_enabled" @@ -720,14 +743,14 @@ for workspace_root in "${workspace_roots_array[@]}"; do fi done <<< "$mount_hex_data" fi - + # If found in DB and disabled, report as disabled (consistent with default mode) if [[ "$found_in_db" == "true" ]] && [[ "$is_enabled" == "false" ]]; then log "Required local .env file is disabled: \"${resolved_path}\"" disabled_mounts+=("$resolved_path|$environment_name") continue fi - + # Check if path exists and is a FIFO if [[ ! -e "$resolved_path" ]] || [[ ! -p "$resolved_path" ]]; then if [[ "$found_in_db" == "true" ]]; then @@ -754,18 +777,18 @@ for workspace_root in "${workspace_roots_array[@]}"; do else # Default mode: Check all local .env files within this workspace from 1Password database log "Using default mode: checking all local .env files in workspace ${workspace_root} from 1Password database" - + if [[ -z "$mount_hex_data" ]]; then log "No mount data available from 1Password database, skipping workspace ${workspace_root}" continue fi - + log "Environment mount data found, checking relevant local .env files for workspace ${workspace_root}..." - + # Process each mount entry from database while IFS= read -r hex_line || [[ -n "$hex_line" ]]; do [[ -z "$hex_line" ]] && continue - + mount_info=$(parse_mount "$hex_line") if [[ -n "$mount_info" ]]; then # Parse mount_info: mount_path|is_enabled|environment_name|uuid|environment_uuid @@ -777,15 +800,15 @@ for workspace_root in "${workspace_roots_array[@]}"; do remaining="${remaining#*|}" uuid="${remaining%%|*}" environment_uuid="${remaining#*|}" - + log "Checking local .env file with id ${uuid} at path \"${mount_path}\" for environment ${environment_uuid} (${environment_name})" - + # Check if this local .env file is relevant to the current workspace if ! is_project_mount "$mount_path" "$workspace_root"; then log "Local .env file does not belong to workspace ${workspace_root}, skipping" continue fi - + if [[ "$is_enabled" == "true" ]]; then if [[ ! -e "$mount_path" ]] || [[ ! -p "$mount_path" ]]; then log "Local .env file is invalid (file is not present or not a FIFO)" @@ -834,18 +857,18 @@ fi # Generate unified error messages if [[ ${#all_missing_invalid[@]} -gt 0 ]] || [[ ${#disabled_mounts[@]} -gt 0 ]]; then permission="deny" - + # Build message for missing/invalid mounts if [[ ${#all_missing_invalid[@]} -gt 0 ]]; then log "Denying permission due to missing or invalid environment files" - + # Extract environment name from DB mounts if available environment_name="" if [[ ${#invalid_mounts[@]} -gt 0 ]]; then first_invalid="${invalid_mounts[0]}" environment_name="${first_invalid#*|}" fi - + if [[ ${#all_missing_invalid[@]} -eq 1 ]]; then if [[ -n "$environment_name" ]]; then agent_message="This project uses 1Password environments. An environment file is expected to be mounted at the specified path. Error: the file is missing or invalid. Environment name: \"${environment_name}\". Path: \"${all_missing_invalid[0]}\". Suggestion: ensure the local .env file is configured and enabled from the environment's destinations tab in the 1Password app." @@ -861,28 +884,28 @@ if [[ ${#all_missing_invalid[@]} -gt 0 ]] || [[ ${#disabled_mounts[@]} -gt 0 ]]; fi fi fi - + # Handle disabled mounts (different issue - needs to be enabled, not configured) if [[ ${#disabled_mounts[@]} -gt 0 ]]; then log "Denying permission due to disabled local .env files" - + # Extract environment name first_disabled="${disabled_mounts[0]}" environment_name="${first_disabled#*|}" - + # Extract mount paths mount_paths=() for mount_entry in "${disabled_mounts[@]}"; do mount_paths+=("${mount_entry%%|*}") done - + if [[ ${#disabled_mounts[@]} -eq 1 ]]; then disabled_msg="Error: the file is not mounted. Environment name: \"${environment_name}\". Path: \"${mount_paths[0]}\". Suggestion: enable the local .env file from the environment's destinations tab in the 1Password app." else file_list=$(IFS=','; echo "${mount_paths[*]}" | sed 's/,/, /g') disabled_msg="Error: these files are not mounted. Environment name: \"${environment_name}\". Paths: \"${file_list}\". Suggestion: enable the local .env files from the environment's destinations tab in the 1Password app." fi - + # Combine messages if we have both missing/invalid and disabled if [[ ${#all_missing_invalid[@]} -gt 0 ]]; then agent_message="${agent_message} ${disabled_msg}" diff --git a/scripts/validate-mounted-env-files.cmd b/scripts/validate-mounted-env-files.cmd new file mode 100644 index 0000000..da45327 --- /dev/null +++ b/scripts/validate-mounted-env-files.cmd @@ -0,0 +1,7 @@ +@echo off +REM On Windows only: tell Cursor to allow the shell command (1Password Environments are not supported here). +REM The hook command omits a file extension, on Windows the OS finds this .cmd file for you. +REM On Mac/Linux the same hook name runs the Bash script next to this file instead. +powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "& { $null = [Console]::In.ReadToEnd(); $m = '1Password Environments local .env validation is only supported on macOS and Linux. Validation was skipped.'; @{ permission = 'allow'; agent_message = $m; user_message = $m } | ConvertTo-Json -Compress }" + +exit /b 0