Back to skill

Security audit

Garmin

Security checks for vulnerabilities and agentic risk

Overview

This Garmin skill is mostly purpose-aligned, but it handles credentials and health data in ways that need careful review before installation.

Review this skill before installing. Use a dedicated low-privilege 1Password item or vault, avoid running the login script until the heredoc credential handling is fixed, replace /tmp/garmin-session with a private 0700 directory, set explicit cache permissions and retention, and install dependencies in a pinned virtual environment rather than system Python.

Vulnerability Patterns
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
Findings (4)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/garmin-login.sh:18
Finding
Python Code Injection Through Credential Interpolation into a Here-Document<![CDATA[ ## Vulnerability Details **File Location**: `scripts/garmin-login.sh`, lines 18-38 **Vulnerability Type**: Python source-code injection through unsafe string interpolation **Risk Level**: High ### Vulnerable Code ```bash EMAIL=$(op item get "$GARMIN_1P_ITEM_NAME" --vault "$GARMIN_1P_VAULT" --fields username 2>/dev/null) PASSWORD=$(op item get "$GARMIN_1P_ITEM_NAME" --vault "$GARMIN_1P_VAULT" --fields password --reveal 2>/dev/null) if [ -z "$EMAIL" ] || [ -z "$PASSWORD" ]; then echo "❌ Credentials not found: '$GARMIN_1P_ITEM_NAME' in vault '$GARMIN_1P_VAULT'" echo " Set GARMIN_1P_ITEM_NAME / GARMIN_1P_VAULT if yours differ." exit 1 fi mkdir -p /tmp/garmin-session python - <<PYEOF import sys, os os.environ['GARMIN_EMAIL'] = "$EMAIL" os.environ['GARMIN_PASSWORD'] = "$PASSWORD" try: from garminconnect import Garmin client = Garmin(os.environ['GARMIN_EMAIL'], os.environ['GARMIN_PASSWORD']) client.login() client.garth.dump(dir_path='/tmp/garmin-session/') PYEOF ``` ### Technical Analysis The username and password obtained from 1Password are inserted directly into dynamically generated Python source code. The here-document delimiter is unquoted, and the credential values are placed inside Python string literals without Python-compatible escaping. Although shell expansion results are not recursively evaluated as new shell syntax, crafted credential content can terminate the Python string and introduce arbitrary Python statements. Ordinary credentials containing quotation marks, backslashes, or line breaks can also corrupt the generated program and cause authentication failures. The vulnerable pattern crosses a trust boundary: data retrieved from a credential store is treated as executable source text rather than as data. ### Attack Path 1. An attacker obtains permission to modify the configured Garmin item in the selected 1Password vault, or convinces the user to select an attacker-controlled item through `GARMIN_1P_ITEM_NAM ...[truncated 1059 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Never interpolate credential values into source code. Pass them as environment variables or through a protected input channel, and quote the here-document delimiter: ```bash export GARMIN_EMAIL="$EMAIL" export GARMIN_PASSWORD="$PASSWORD" python - <<'PYEOF' import os import sys from garminconnect import Garmin email = os.environ["GARMIN_EMAIL"] password = os.environ["GARMIN_PASSWORD"] client = Garmin(email, password) client.login() PYEOF ``` Additional hardening should include: 1. Unset `GARMIN_EMAIL` and `GARMIN_PASSWORD` immediately after the Python process exits. 2. Restrict the 1Password service account to the single required vault item where supported. 3. Avoid placing secret values in command-line arguments, logs, or exception messages. 4. Prefer implementing authentication entirely in Python, as already done in `get-stats.py`, so credentials never become generated program text. 5. Add tests using passwords containing quotes, backslashes, dollar signs, and line breaks. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/garmin-login.sh:27
Finding
Garmin Authentication State Stored in a Predictable Shared Temporary Directory<![CDATA[ ## Vulnerability Details **File Location**: `scripts/garmin-login.sh`, lines 27-38 **Vulnerability Type**: Unsafe temporary storage of sensitive session material **Risk Level**: Medium ### Vulnerable Code ```bash mkdir -p /tmp/garmin-session python - <<PYEOF import sys, os os.environ['GARMIN_EMAIL'] = "$EMAIL" os.environ['GARMIN_PASSWORD'] = "$PASSWORD" try: from garminconnect import Garmin client = Garmin(os.environ['GARMIN_EMAIL'], os.environ['GARMIN_PASSWORD']) client.login() client.garth.dump(dir_path='/tmp/garmin-session/') PYEOF ``` The same fixed location is used in `scripts/get-stats.py`, lines 51-57: ```python auth_dir = '/tmp/garmin-session/' os.makedirs(auth_dir, exist_ok=True) try: client = Garmin(email, password) client.login() client.garth.dump(dir_path=auth_dir) ``` ### Technical Analysis Garmin authentication state is dumped into the predictable path `/tmp/garmin-session/`. The code does not: - Create the directory with an explicit `0700` mode. - Confirm that the directory is owned by the current user. - Reject symbolic links or an existing attacker-controlled path. - Set a restrictive process umask before creating session files. - Delete or expire the authentication state after use. `/tmp` is normally shared by local users. The exact file permissions may depend on the process umask and behavior of the `garth` dependency, but the Skill does not enforce the confidentiality requirements itself. The documentation describes the session as temporary, yet no cleanup or expiration is implemented. ### Attack Path 1. A local attacker predicts the fixed `/tmp/garmin-session` path. 2. Before the Skill runs, the attacker creates or manipulates that path where operating-system permissions permit, or monitors it after creation. 3. The user authenticates through `garmin-login.sh` or `get-stats.py`. 4. The Garmin library writes reusable authentication state into the fixed directory. 5. If permissions, ownership, ...[truncated 673 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Store authentication state in a private per-user state directory instead of a shared fixed `/tmp` path. For example: ```bash SESSION_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/garmin/session" install -d -m 700 "$SESSION_DIR" ``` If temporary storage is required, create it securely: ```bash umask 077 SESSION_DIR="$(mktemp -d "${TMPDIR:-/tmp}/garmin-session.XXXXXXXX")" trap 'rm -rf -- "$SESSION_DIR"' EXIT export GARMIN_SESSION_DIR="$SESSION_DIR" ``` The Python code should then read `GARMIN_SESSION_DIR` rather than use a hard-coded path. Additional controls should include: 1. Verify that an existing directory is owned by the current effective user. 2. Reject symbolic links and unexpected file types. 3. Enforce mode `0700` on the directory and `0600` on session files. 4. Implement explicit expiration and deletion of authentication artifacts. 5. Document the session lifetime and whether tokens can be reused. 6. Avoid separate session dumps unless they are necessary for the declared operation. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/cache-daily.sh:5
Finding
Sensitive Health Metrics Persisted Without Explicit Access or Retention Controls<![CDATA[ ## Vulnerability Details **File Location**: `scripts/cache-daily.sh`, lines 5-23 **Vulnerability Type**: Insecure local storage of sensitive health information **Risk Level**: Medium ### Vulnerable Code ```bash SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" TODAY=$(date +%Y-%m-%d) CACHE_DIR="${GARMIN_CACHE_DIR:-/root/clawd/data/fitness/garmin}" CACHE_FILE="$CACHE_DIR/$TODAY.json" mkdir -p "$CACHE_DIR" STATS=$("$SCRIPT_DIR/get-stats.sh" 2>/dev/null) if [ -z "$STATS" ] || echo "$STATS" | grep -q '"error"'; then echo "⚠️ Could not fetch Garmin data" exit 1 fi CACHED=$(echo "$STATS" | jq ". + {cached_at: \"$(date -Iseconds)\", date: \"$TODAY\"}") echo "$CACHED" > "$CACHE_FILE" echo "✅ Cached Garmin data for $TODAY" echo "$CACHED" | jq . ``` ### Technical Analysis The script persistently stores Garmin-derived sleep, heart-rate, stress, training, and body-battery information in JSON files. It creates the cache directory and files without explicitly enforcing restrictive permissions. Consequently, confidentiality depends on the caller's umask and the permissions of all parent directories. The write is also non-atomic, allowing readers to observe partially written data and making replacement or redirection risks more significant if another principal can modify the cache directory. The implementation additionally conflicts with the privacy statement that data is queried on demand and not stored long-term by the Skill. The bundled `cache-daily.sh` script performs persistent storage and has no retention or deletion policy. ### Attack Path 1. The user runs `scripts/cache-daily.sh`, directly or through external automation. 2. The script retrieves detailed Garmin health metrics. 3. It creates a dated JSON file using the process's ambient umask and existing directory permissions. 4. Another local principal with read access to the directory or file reads the persisted health data. 5. Because no retention policy exists, historical records a ...[truncated 819 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Make local persistence explicit, opt-in, and access-controlled: ```bash umask 077 install -d -m 700 "$CACHE_DIR" TMP_FILE="$(mktemp "$CACHE_DIR/.${TODAY}.json.XXXXXX")" printf '%s\n' "$CACHED" > "$TMP_FILE" chmod 600 "$TMP_FILE" mv -f -- "$TMP_FILE" "$CACHE_FILE" ``` Further hardening should include: 1. Validate that `CACHE_DIR` is owned by the expected user and is not a symbolic link. 2. Enforce directory mode `0700` and file mode `0600`. 3. Implement configurable retention and secure deletion procedures. 4. Obtain clear user consent before enabling historical storage. 5. Update `SKILL.md` to state precisely what is stored, where it is stored, and for how long. 6. Avoid printing the complete cached health record to standard output unless explicitly requested. 7. Consider storing only the minimum metrics required for trend analysis rather than the complete response. ]]>

T08 · Insecure Dependencies

Warning
Location
SKILL.md:25
Finding
Unpinned Third-Party Dependency Installed into System Python<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md`, lines 25-30 **Vulnerability Type**: Unpinned dependency and unsafe system-package installation guidance **Risk Level**: Medium ### Vulnerable Code ```bash pip3 install garminconnect --break-system-packages # Or using a virtual environment (recommended): # python3 -m venv ./venv # source ./venv/bin/activate # pip install garminconnect ``` ### Technical Analysis The setup instructions install the latest available `garminconnect` release without a version constraint, lock file, or package hash. The primary command also uses `--break-system-packages`, bypassing protections for an externally managed Python installation. The dependency executes in a sensitive context: it receives the user's Garmin email and password, creates authentication sessions, and processes private health information. A compromised package release, compromised distribution account, or unexpected future update would therefore execute with direct access to these assets. No evidence was found that the project intentionally selects a known malicious package. The risk arises from insufficient supply-chain controls and unnecessary modification of the system Python environment. ### Attack Path 1. The package repository, maintainer account, or a future `garminconnect` release is compromised. 2. A user follows the documented unpinned installation command. 3. `pip` resolves and installs the compromised release. 4. Malicious package installation hooks or imported runtime code execute with the user's privileges. 5. The package can access Garmin credentials, session artifacts, environment variables, health data, and files available to the Skill runner. The `--break-system-packages` option can additionally overwrite or conflict with system-managed Python components, increasing the effect of dependency compromise or incompatibility. ### Impact Assessment A malicious dependency could achieve arbitrary code execution with the privileges ...[truncated 394 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Use an isolated virtual environment and a reviewed, pinned dependency set: ```bash python3 -m venv ./venv ./venv/bin/python -m pip install --upgrade pip ./venv/bin/python -m pip install --require-hashes -r requirements.txt ``` The requirements file should pin an audited version and all transitive dependencies with cryptographic hashes. For example, after independently verifying the selected release: ```text garminconnect==<reviewed-version> \ --hash=sha256:<verified-distribution-hash> ``` Additional controls should include: 1. Remove `--break-system-packages` from the recommended installation path. 2. Commit a lock file or fully hashed requirements file. 3. Review dependency updates before adoption instead of automatically using the newest release. 4. Use trusted package indexes with TLS and, where feasible, an organization-controlled package mirror. 5. Run dependency vulnerability and provenance checks in continuous integration. 6. Execute the Skill under a dedicated, unprivileged account with access only to the required Garmin credential and data directories. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
Findings (19)

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
The skill claims to be a Garmin integration but also describes combined Garmin+Strava briefing behavior without declaring that external integration. This is a capability-boundary mismatch that can cause unintended cross-service data aggregation and surprise users about what accounts and data sources are involved.

Tp4

High
Category
MCP Tool Poisoning
Confidence
93% confidence
Finding
The skill claims to be a Garmin integration but also describes combined Garmin+Strava briefing behavior without declaring that external integration. This is a capability-boundary mismatch that can cause unintended cross-service data aggregation and surprise users about what accounts and data sources are involved.

Env Variable Harvesting

High
Category
Data Exfiltration
Content
result = subprocess.run(
        ['op', 'item', 'get', item_name, '--vault', vault, '--format', 'json'],
        capture_output=True, text=True,
        env={**os.environ, 'OP_SERVICE_ACCOUNT_TOKEN': op_token}
    )
    if result.returncode != 0:
        _die(f"1Password lookup failed for '{item_name}': {result.stderr.strip()}")
Confidence
60% confidence
Finding
Code enumerates, copies, or searches environment variables for secrets. Bulk environment access can collect credentials unrelated to the skill's stated purpose.

Lp3

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding
The skill describes shell commands, environment-variable use, and local file/token handling, but it declares no explicit tool scope or permissions. In an agent setting, undeclared shell/env capability increases the chance of over-privileged execution and makes review of what the skill can access much harder.

Session Persistence

Medium
Category
Rogue Agent
Content
### 2. Store Credentials in 1Password

Create a new "Login" item in your 1Password vault (e.g., "Personal") with the following details:

*   **Title:** `Garmin Connect` (or a custom name you prefer)
*   **Username:** Your Garmin Connect email address
Confidence
72% confidence
Finding
The skill directs use of long-lived credentials in 1Password and mentions temporary session-token caching, which introduces persistence of authentication material beyond a single run. While this is common operational practice, it still increases exposure if local storage, cache directories, or the 1Password integration are misconfigured or accessed by other processes.

Missing User Warnings

Medium
Confidence
85% confidence
Finding
The skill processes sensitive health data including sleep, heart rate, stress, and recovery metrics, yet the privacy and caching implications are only mentioned later and weakly. Users may run the skill before understanding that health data and session artifacts may be cached locally, increasing confidentiality and compliance risk.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
This shell script persists fetched Garmin metrics to disk using a dated JSON file, which is a safety-relevant data write involving personal fitness information. Although the script prints a success message after caching, it does not disclose beforehand that user data will be stored locally or describe the privacy impact in comments beyond a terse one-line summary.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
This shell script reads a 1Password service account token and retrieves Garmin credentials, then uses them to perform a remote login. While failures are printed, there is no user-facing disclosure before the sensitive credential access and outbound authentication, and this file contains no comment or prompt warning that secrets will be read and used automatically.

Missing User Warnings

Medium
Confidence
97% confidence
Finding
The script writes authenticated session material to /tmp/garmin-session via client.garth.dump(), which creates local persistence of login state in a world-accessible temporary area. On multi-user systems or in environments with weak tmp hygiene, other local processes or users may read, copy, or reuse these session artifacts to access the Garmin account without the password.

Context-Inappropriate Capability

Medium
Confidence
90% confidence
Finding
The skill description says it fetches Garmin analytics, but the implementation also reaches into 1Password using a service-account token from the environment. This undisclosed secret-access capability is security-relevant because it broadens trust requirements and may surprise users or orchestrators that did not intend to grant password-manager access to a fitness stats skill.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The script accesses OP_SERVICE_ACCOUNT_TOKEN and uses it to retrieve credentials from 1Password without any explicit disclosure in the skill context. In an agent ecosystem, undisclosed secret access is dangerous because users may authorize a benign-seeming Garmin skill without realizing it can consume password-manager-backed credentials.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
if not op_token:
        _die("OP_SERVICE_ACCOUNT_TOKEN not set")

    result = subprocess.run(
        ['op', 'item', 'get', item_name, '--vault', vault, '--format', 'json'],
        capture_output=True, text=True,
        env={**os.environ, 'OP_SERVICE_ACCOUNT_TOKEN': op_token}
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Tainted flow: 'item_name' from os.getenv (line 25, credential/environment) → subprocess.run (code execution)

Medium
Category
Data Flow
Content
if not op_token:
        _die("OP_SERVICE_ACCOUNT_TOKEN not set")

    result = subprocess.run(
        ['op', 'item', 'get', item_name, '--vault', vault, '--format', 'json'],
        capture_output=True, text=True,
        env={**os.environ, 'OP_SERVICE_ACCOUNT_TOKEN': op_token}
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The code persists Garmin session material to /tmp/garmin-session/, which may expose reusable authentication state to other local users/processes depending on file permissions and host configuration. Writing auth artifacts to a predictable shared temporary path without user disclosure increases the risk of session theft or unintended retention of sensitive health-account access.

Description-Behavior Mismatch

Medium
Confidence
92% confidence
Finding
The manifest describes a Garmin-focused skill for fetching and analyzing Garmin Connect health and recovery metrics. This script also invokes a separate Strava skill to include external activity data, which broadens behavior beyond the stated Garmin integration scope.

Natural-Language Policy Violations

Low
Confidence
93% confidence
Finding
Multiple user-facing examples and descriptions refer specifically to 'Brian' (for example, asking whether Brian is recovered enough to train hard). This hard-coded personalization can violate language/locale-style organizational expectations for generic, reusable skills unless such targeting is explicitly justified.

Natural-Language Policy Violations

Low
Confidence
93% confidence
Finding
The usage description states that the command returns whether Brian is recovered enough for hard training. This is a natural-language policy concern because the skill presents individualized wording instead of neutral language suitable for broader use.

Intent-Code Divergence

Low
Confidence
81% confidence
Finding
The top-level docstring presents the script as only fetching current Garmin Connect daily stats. In reality, the script also pulls credentials from 1Password, logs into Garmin Connect, and dumps session data to `/tmp/garmin-session/`, which materially broadens what the script does beyond the documented intent.

Missing User Warnings

Low
Confidence
79% confidence
Finding
The script invokes other scripts to retrieve Garmin and Strava activity data, which are potentially privacy-sensitive data sources. Although the header comment mentions Garmin recovery and optional Strava activity, there is no user-facing prompt or runtime disclosure that personal fitness data is being accessed and displayed.

Static analysis

No suspicious patterns detected.