T09 · Insecure Skill Coding Practices
- Location
scripts/webhook_server.py:41- Finding
Public Webhook Accepts Unauthenticated and Forged Events
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
This WHOOP connector is mostly purpose-aligned, but it handles sensitive health data and tokens while documenting an unauthenticated public webhook setup and weak local secret/storage practices.
Review before installing. This skill is not clearly malicious, but it will access and store sensitive WHOOP health, profile, body, and token data. Avoid enabling the public webhook server unless you add authentication, rate limiting, and replay protection; keep credentials out of systemd unit files and shared shell profiles; and treat ~/.whoop/whoop.db and token.json as sensitive local files.
scripts/webhook_server.py:41Public Webhook Accepts Unauthenticated and Forged Events
scripts/whoop_client.py:155OAuth Authorization State Is Generated but Not Validated
scripts/db.py:14Local Health Database Permissions Are Not Explicitly Restricted
scripts/install.sh:22Runtime Dependencies Are Installed Without Version or Integrity Pinning
references/setup-guide.md:79Deployment Guide Embeds the WHOOP Client Secret in a Systemd Unit
The declared purpose centers on retrieving and storing WHOOP biometric/workout data for use in OpenClaw. This code chunk does not perform that primary function. Instead, it implements a setup wizard and config manager: it loads/saves configuration, prompts for preferences, enables webhook-related options, and outputs config JSON. While these settings may support a WHOOP integration, the actual behavior here is materially different from the declared main capability. Therefore this chunk is a description-behavior mismatch.
The README instructs users to provision long-lived OAuth client credentials via environment variables, which creates a session persistence and secret-handling risk if the host environment is shared, logged, or inspected by other processes. In the context of a health-data skill, compromise of these credentials could enable continued access to sensitive WHOOP data and token minting beyond a single interactive session.
## Why do I need a developer account?
WHOOP doesn't have a public API. To let your agent read your data, you need to create a free developer app on WHOOP's platform (takes ~5 minutes). This gives you OAuth credentials so the skill can read **your own data only**. All data stays local — nothing is uploaded.
## Setup
The setup section states the OAuth credentials let the skill read the user's own data only and that all data stays local. Elsewhere, the README explicitly lists a webhook server and real-time webhooks as features, which adds an inbound network service and event-reception behavior beyond a simple local data reader/sync tool.
The wording promises a narrowly scoped, read-only local data access model. The same README later documents a webhook server/event receiver and push notifications, which contradict the impression that the skill only performs local reads of user-owned data via OAuth.
The skill requests or implies use of powerful capabilities—environment variables, shell execution, network access, and local file writes—without declaring any explicit tool scope or permission boundaries. That makes it harder for the host to enforce least privilege and increases the chance an agent can execute setup/auth/sync commands or write config/state files without clear user-visible constraints.
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.
## Why do I need a developer account?
WHOOP doesn't have a public API — you can't just pull your data with a simple request. To let your AI agent read your recovery, sleep, and workout data, you need to create a free "developer app" on WHOOP's platform. This gives you a pair of credentials (Client ID + Client Secret) that authorize the skill to access **your own data only**, using the industry-standard OAuth protocol.
- It's free and takes about 5 minutes
- You're only granting **read access** to your own account
Line L009 gives an absolute assurance that data is stored locally and "nothing is uploaded anywhere." Later lines describe configuring a public HTTPS server and registering a webhook URL with WHOOP so data/events are transmitted to that endpoint, which contradicts the earlier plain-language claim.
The manifest describes a skill for connecting a WHOOP wearable to fetch and store recovery, sleep, HRV, strain, and workout data locally. This guide adds an optional deployment model involving a publicly exposed webhook server, reverse proxy, health endpoint, systemd service, and scheduled sync infrastructure, which goes materially beyond simple local data retrieval/storage.
The module docstring presents this daemon as part of WHOOP Connect's syncing/storage behavior, but the implementation in the main loop only calls API methods, compares IDs from the local DB, and prints new recovery/sleep/workout events. There is no code here that inserts or updates the local database with newly fetched records, which is a material mismatch with the stated purpose of fetching and storing data locally.
This module persistently stores highly sensitive health and profile data locally, including recovery metrics, sleep details, HRV, heart rate, email, and body measurements, but the code shows no encryption, access controls, retention limits, or consent/disclosure mechanism. In the context of a wearable-data connector, local persistence is expected, but storing regulated-style health data in plaintext under a predictable path increases privacy and compromise risk if the host account or filesystem is accessed.
Skill attempts to fill the context window with filler content, displacing legitimate instructions and safety constraints. This can degrade agent performance or bypass safety boundaries.
sleep_consistency_percentage,
baseline_milli, need_from_sleep_debt_milli,
need_from_recent_strain_milli, need_from_recent_nap_milli)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
data["id"], data.get("cycle_id"), data["user_id"],
data["created_at"], data["updated_at"], data["start"], data["end"],
Skill attempts to fill the context window with filler content, displacing legitimate instructions and safety constraints. This can degrade agent performance or bypass safety boundaries.
percent_recorded,
zone_zero_milli, zone_one_milli, zone_two_milli,
zone_three_milli, zone_four_milli, zone_five_milli)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
data["id"], data["user_id"], data["created_at"], data["updated_at"],
data["start"], data["end"], data["timezone_offset"],
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.
exit 1
fi
# Create venv if it doesn't exist
if [ ! -d "$VENV_DIR" ]; then
echo "Creating virtual environment..."
python3 -m venv "$VENV_DIR"
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.
# Create data directory
mkdir -p ~/.whoop
chmod 700 ~/.whoop
echo "✓ Dependencies installed (venv at ${VENV_DIR})"
echo ""
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
from db import get_db, upsert_recovery, upsert_sleep, upsert_workout, upsert_cycle, upsert_profile, upsert_body, get_daily_api_calls, increment_api_calls
from formatters import format_recovery, format_sleep, format_workout, format_cycle, format_profile, format_trends
BASE_URL = "https://api.prod.whoop.com/developer"
AUTH_URL = "https://api.prod.whoop.com/oauth/oauth2/auth"
TOKEN_URL = "https://api.prod.whoop.com/oauth/oauth2/token"
REDIRECT_URI = "http://localhost:3000/callback"
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
from db import get_db, upsert_recovery, upsert_sleep, upsert_workout, upsert_cycle, upsert_profile, upsert_body, get_daily_api_calls, increment_api_calls
from formatters import format_recovery, format_sleep, format_workout, format_cycle, format_profile, format_trends
BASE_URL = "https://api.prod.whoop.com/developer"
AUTH_URL = "https://api.prod.whoop.com/oauth/oauth2/auth"
TOKEN_URL = "https://api.prod.whoop.com/oauth/oauth2/token"
REDIRECT_URI = "http://localhost:3000/callback"
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
from db import get_db, upsert_recovery, upsert_sleep, upsert_workout, upsert_cycle, upsert_profile, upsert_body, get_daily_api_calls, increment_api_calls
from formatters import format_recovery, format_sleep, format_workout, format_cycle, format_profile, format_trends
BASE_URL = "https://api.prod.whoop.com/developer"
AUTH_URL = "https://api.prod.whoop.com/oauth/oauth2/auth"
TOKEN_URL = "https://api.prod.whoop.com/oauth/oauth2/token"
REDIRECT_URI = "http://localhost:3000/callback"
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
from db import get_db, upsert_recovery, upsert_sleep, upsert_workout, upsert_cycle, upsert_profile, upsert_body, get_daily_api_calls, increment_api_calls
from formatters import format_recovery, format_sleep, format_workout, format_cycle, format_profile, format_trends
BASE_URL = "https://api.prod.whoop.com/developer"
AUTH_URL = "https://api.prod.whoop.com/oauth/oauth2/auth"
TOKEN_URL = "https://api.prod.whoop.com/oauth/oauth2/token"
REDIRECT_URI = "http://localhost:3000/callback"
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
from db import get_db, upsert_recovery, upsert_sleep, upsert_workout, upsert_cycle, upsert_profile, upsert_body, get_daily_api_calls, increment_api_calls
from formatters import format_recovery, format_sleep, format_workout, format_cycle, format_profile, format_trends
BASE_URL = "https://api.prod.whoop.com/developer"
AUTH_URL = "https://api.prod.whoop.com/oauth/oauth2/auth"
TOKEN_URL = "https://api.prod.whoop.com/oauth/oauth2/token"
REDIRECT_URI = "http://localhost:3000/callback"
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
from db import get_db, upsert_recovery, upsert_sleep, upsert_workout, upsert_cycle, upsert_profile, upsert_body, get_daily_api_calls, increment_api_calls
from formatters import format_recovery, format_sleep, format_workout, format_cycle, format_profile, format_trends
BASE_URL = "https://api.prod.whoop.com/developer"
AUTH_URL = "https://api.prod.whoop.com/oauth/oauth2/auth"
TOKEN_URL = "https://api.prod.whoop.com/oauth/oauth2/token"
REDIRECT_URI = "http://localhost:3000/callback"
The OAuth scope list includes read:profile and read:body_measurement, which grants access to personal and biometric data not clearly needed for the advertised functionality. Overbroad scopes increase the blast radius of token compromise and create unnecessary exposure of sensitive health-related information.
OAuth access and refresh tokens are persisted to ~/.whoop/token.json without any user-facing disclosure that credentials will be stored for ongoing reuse. Even with file mode 0600, silent persistence of long-lived refresh tokens can surprise users and increases risk if the local account or filesystem is later compromised.
The client silently writes retrieved recovery, sleep, workout, cycle, profile, and body data into a local database without any visible consent, retention notice, or storage controls. Because this is sensitive health and biometric information, undisclosed persistence materially increases privacy harm and the consequences of local compromise or secondary use.
The skill fetches and stores WHOOP profile and body data even though the stated skill purpose emphasizes recovery, sleep, HRV, strain, and workout data. Collecting additional health/profile attributes beyond the declared scope violates data minimization and increases privacy risk because users may authorize more sensitive data than necessary without clear expectation.
The README lists generic natural-language requests such as "How's my recovery today?" and "How did I sleep last night?" without clarifying whether these are merely examples or the actual invocation triggers. Because these phrases overlap with ordinary conversation, an agent or router could match them more broadly than intended.
No suspicious patterns detected.