T05 · Unauthorized Access and Privilege Escalation
- Location
scripts/log_to_obsidian.py:149- Finding
Vault-Wide Git Staging and Push Can Upload Unrelated Sensitive Files
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
This WHOOP health-data skill is mostly coherent, but it needs review because it can persist health API credentials, write health data into notes, push an entire Obsidian vault to git, and open/share chart files that load remote code.
Install only if you are comfortable granting broad read access to WHOOP health data and storing refresh credentials locally. Before using Obsidian logging, put WHOOP notes in a dedicated vault or disable git sync, because the current script can commit and push unrelated vault files. Treat generated chart HTML as sensitive health data, avoid auto-sharing it, and prefer offline or locally bundled chart assets. Revoke WHOOP app access and delete ~/.config/whoop-skill/credentials.json if you stop using it.
scripts/log_to_obsidian.py:149Vault-Wide Git Staging and Push Can Upload Unrelated Sensitive Files
scripts/log_to_obsidian.py:126Unvalidated Date Argument Enables Path Traversal Outside the Daily Notes Directory
scripts/chart.py:174Mutable Remote JavaScript Executes in HTML Containing Private Health Data
scripts/chart.py:465Predictable Shared Temporary Chart Files Permit Local Disclosure and Symlink Overwrites
requirements.txt:1Unpinned Python Dependency Makes Installation Non-Reproducible
This finding is valid to the extent that the skill documents local file modification and git synchronization behavior without clearly surfacing them in a permission model or high-visibility warning. Silent or insufficiently disclosed writes to an Obsidian vault and remote git push can affect user data integrity and confidentiality, especially because health data is involved.
This finding is valid to the extent that the skill documents local file modification and git synchronization behavior without clearly surfacing them in a permission model or high-visibility warning. Silent or insufficiently disclosed writes to an Obsidian vault and remote git push can affect user data integrity and confidentiality, especially because health data is involved.
This finding is valid to the extent that the skill documents local file modification and git synchronization behavior without clearly surfacing them in a permission model or high-visibility warning. Silent or insufficiently disclosed writes to an Obsidian vault and remote git push can affect user data integrity and confidentiality, especially because health data is involved.
The skill stores OAuth tokens in a local credentials.json file, which is a sensitive secret store for access to personal health data. If file permissions are weak, the path is exposed, or the vault/config points somewhere unsafe, an attacker or another local process could steal tokens and access WHOOP account data.
~/.config/whoop-skill/
credentials.json — OAuth tokens (created by auth.py on first setup)
experiments.json — experiment tracking data (created on first `plan` command)
config.json — optional path/timezone overrides (copy from config.example.json)
Automatically creating and managing a credentials file for OAuth tokens creates persistent secret material on disk. Persistent token storage raises the risk of credential theft and long-term account access if the machine, backup system, or synced home directory is compromised.
config.json — optional path/timezone overrides (copy from config.example.json)
The directory and `credentials.json` are created automatically when you run `scripts/auth.py`. You never need to create them manually.
## Setup
The setup flow collects a Client ID and Client Secret and saves resulting credentials locally, which is sensitive account-linked material. If mishandled, these secrets can enable unauthorized token minting or API access, particularly because the skill requests offline access.
1. Prompt you for your Client ID and Client Secret
2. Ask which callback method you chose in Step 1 (local server or manual)
3. Walk you through the authorization flow
4. Save credentials to `~/.config/whoop-skill/credentials.json`
**Customize paths (optional):**
Copy `config.example.json` from the skill root to `~/.config/whoop-skill/config.json` and edit to override defaults:
Allowing override of creds_path via config increases flexibility but can also redirect token storage to insecure or synced locations. That makes accidental disclosure more likely, especially if users point it at broadly readable directories or cloud-synced folders.
Copy config.example.json from the skill root to ~/.config/whoop-skill/config.json and edit to override defaults:
{
"creds_path": "~/.config/whoop-skill/credentials.json",
"vault_path": "~/my-obsidian-vault",
"daily_notes_subdir": "Daily Notes",
"timezone": "America/New_York",
The documented workflow requires loading stored credentials and updating them during refresh, meaning the skill routinely accesses sensitive tokens in plaintext-like local storage. Regular automated credential handling increases exposure surface to logs, crashes, backups, or other local software.
## Workflow
1. Load credentials from `~/.config/whoop-skill/credentials.json`
2. If `expires_at` is in the past (or within 60s), call `scripts/refresh_token.py` to get a new access token and update the file
3. Call the appropriate endpoint (see `references/api.md`)
4. Parse and present the data in plain language
Use of an access token to call the API is expected, but in this context it is security-sensitive because the token authorizes retrieval of personal health data. If exposed through logs, files, subprocess environments, or crash output, it enables unauthorized data access until expiry and potentially beyond if paired with refresh credentials.
## Workflow
1. Load credentials from `~/.config/whoop-skill/credentials.json`
2. If `expires_at` is in the past (or within 60s), call `scripts/refresh_token.py` to get a new access token and update the file
3. Call the appropriate endpoint (see `references/api.md`)
4. Parse and present the data in plain language
Automatic token refresh extends the lifetime of API access and therefore raises the consequences of local secret compromise. Because the account data includes sensitive wellness and biometrics information, unauthorized persistence materially increases privacy risk.
## Token Refresh
Run `scripts/refresh_token.py` when the access token is expired. It reads/writes `~/.config/whoop-skill/credentials.json` automatically.
To re-auth from scratch, run `scripts/auth.py` again.
Automatic token refresh extends the lifetime of API access and therefore raises the consequences of local secret compromise. Because the account data includes sensitive wellness and biometrics information, unauthorized persistence materially increases privacy risk.
## Token Refresh
Run `scripts/refresh_token.py` when the access token is expired. It reads/writes `~/.config/whoop-skill/credentials.json` automatically.
To re-auth from scratch, run `scripts/auth.py` again.
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
{
"creds_path": "~/.config/whoop-skill/credentials.json",
"vault_path": "~/path/to/your/obsidian-vault",
"daily_notes_subdir": "Daily Notes",
"timezone": "America/New_York",
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
{
"creds_path": "~/.config/whoop-skill/credentials.json",
"vault_path": "~/path/to/your/obsidian-vault",
"daily_notes_subdir": "Daily Notes",
"timezone": "America/New_York",
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
{
"creds_path": "~/.config/whoop-skill/credentials.json",
"vault_path": "~/path/to/your/obsidian-vault",
"daily_notes_subdir": "Daily Notes",
"timezone": "America/New_York",
The script stores long-lived OAuth material, including client secret and refresh token, in a local JSON file under the user's home directory. Even with chmod 600, plaintext at-rest credential storage increases risk of token theft from local compromise, backups, logs, or multi-process access, especially because the skill handles health data and offline access.
Guides you through connecting your WHOOP account:
1. Prompts for your WHOOP Developer App client ID and secret
2. Opens a browser to authorize access (local server or manual code paste)
3. Saves tokens to ~/.config/whoop-skill/credentials.json
Run this once to get set up. Tokens are refreshed automatically by other scripts.
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
print()
print(f"✓ Setup complete! Credentials saved to {creds_path}")
print(f" Access token expires: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(creds['expires_at']))}")
print()
print("You're all set. Ask your agent about your WHOOP data!")
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
#!/usr/bin/env python3
"""Refresh the WHOOP access token using the stored refresh token."""
import json
import time
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
#!/usr/bin/env python3
"""Refresh the WHOOP access token using the stored refresh token."""
import json
import time
The skill explicitly describes capabilities that require shell, file read/write, network access, and credential handling, yet it declares no tool scope or permission boundaries. This is dangerous because an agent may invoke powerful operations without transparent least-privilege constraints, increasing the chance of unintended file modification, credential exposure, or network actions.
The 'Use when' language is overly broad for a skill that can access health data, local files, credentials, shell, and network resources. Overbroad activation criteria increase the chance the agent will invoke this skill in loosely related health conversations, causing unnecessary access to sensitive data or triggering side effects outside user intent.
The skill handles sensitive health information, stores OAuth tokens locally, and may append health data into an Obsidian vault and push it to a git remote, but the documentation lacks a concise warning about privacy, persistence, and external disclosure risks. Users may not realize their biometric data and tokens are being stored and potentially synchronized beyond the local machine.
The skill is designed to create and keep credential state across sessions, including offline-capable tokens. Session persistence is not inherently wrong for OAuth, but it becomes security-relevant here because persisted auth for a health-data service enables background or future access without renewed user review.
config.json — optional path/timezone overrides (copy from config.example.json)
The directory and `credentials.json` are created automatically when you run `scripts/auth.py`. You never need to create them manually.
## Setup
The documentation explicitly instructs requesting all available WHOOP scopes, including profile, body measurements, and offline refresh-token access, regardless of whether a given workflow needs them. This violates least-privilege principles and increases privacy and persistence risk because the skill handles sensitive health data plus long-lived authorization, with no warning or guidance to minimize requested access.
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.
print("ERROR: No code entered.")
sys.exit(1)
print("Exchanging code for tokens...")
resp = requests.post(TOKEN_URL, data={
"grant_type": "authorization_code",
"code": manual_code,
"client_id": client_id,
The output file path is taken directly from the WHOOP_EXPERIMENTS_FILE environment variable and then opened for writing without validation. If an attacker can influence the environment in which this skill runs, they can redirect writes to arbitrary filesystem locations accessible to the process, causing clobbering of files, data corruption, or overwriting sensitive user configuration.
def save_experiments(experiments):
EXPERIMENTS_FILE.parent.mkdir(parents=True, exist_ok=True)
with open(EXPERIMENTS_FILE, "w") as f:
json.dump(experiments, f, indent=2)
No suspicious patterns detected.