T09 · Insecure Skill Coding Practices
- Location
internal/config/config.go:199- Finding
Arbitrary API base URL can receive authenticated Google Health requests
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
This Google Health reader discloses most sensitive behavior, but needs review because configuration can send logged-in requests to a non-Google server or ask for broader Google permissions.
Review carefully before installing. Only use it with your own Google account, protect config.json and token.json, avoid running it from untrusted directories, and do not set or accept custom base_url, GOOGLE_HEALTH_BASE_URL, or unexpected scopes unless you fully trust that configuration. Check the Google consent screen for exactly the intended read-only Health scopes.
internal/config/config.go:199Arbitrary API base URL can receive authenticated Google Health requests
internal/config/config.go:199Configurable OAuth scopes can exceed the declared read-only Google Health permissions
The declared description is about a self-contained read-only client for Google Health v4 that authenticates, reads health data, and returns JSON. The supplied code chunk instead implements a shell-completion command for the CLI. This is a materially different purpose from the declared functionality and introduces an undeclared capability (generation of shell completion scripts). While shell completion can be a supporting feature of a CLI, this specific chunk does not perform any of the described Google Health behaviors, so the code chunk does not accurately represent the declared description.
The declared description presents the skill as a self-contained, read-only client for fetching Google Health API data and arbitrary read-only v4 API paths, emphasizing that it does no filtering, merging, or writing. The supplied code chunk instead defines a doctor command whose primary function is local diagnostics: it resolves config, loads a cached token, checks whether a current token can be obtained, and emits JSON containing token validity plus local file paths, configured user, scopes, base URL, and version. It also prints remediation guidance for missing OAuth client credentials or missing authentication. While token handling is a supporting aspect of an OAuth-based client, this code's behavior is materially different from the declared health-data retrieval purpose and includes output of sensitive local/account metadata not mentioned in the description. Therefore this chunk is a description-behavior mismatch.
This code chunk does not implement the declared read-only Google Health client behavior. Instead, it is a documentation test/guardrail ensuring certain warning text exists in markdown files. That is a materially different primary purpose from reading Google Health data, authenticating with OAuth2, or exposing v4 API GET functionality. The file access shown is limited to local repository metadata and documentation, which is inconsistent with the declared external API-reading behavior. While such tests may be part of the same repository, this specific supplied code chunk does not match the declared skill functionality.
The supplied code chunk does not implement a Google Health API client, OAuth2 authentication, JSON data retrieval, daily roll-ups, exercise parsing, or read-only API path access. Instead, it is a unit/privacy test that walks local testdata files and checks them for realistic identifiers to enforce synthetic fixture hygiene. This is a materially different purpose and behavior from the declared description, so it is a clear mismatch.
The supplied code is a narrow CLI utility command for outputting build version metadata. Its behavior is unrelated to the declared primary purpose of a self-contained Google Health read-only client. While a larger project could still contain the declared functionality elsewhere, this specific code chunk does not match that description and instead implements an unrelated command.
The declared description is for a self-contained read-only Google Health API client. The supplied code chunk does not implement any health-data access, API communication, OAuth, JSON API retrieval, or read-only endpoint handling. Instead, it is a version metadata utility that exposes build information such as version, commit, date, and Go runtime version. This is a materially different primary purpose, so the description does not accurately represent this code chunk.
The declared description is for a self-contained, read-only Google Health API client. The supplied code does something entirely different: it is a Python script for detecting drift and synchronizing skill package files across repository subtrees. It interacts with the local filesystem and Git, not Google services; it has no OAuth, HTTP requests, API client logic, or health-data parsing. It also supports destructive actions such as copying and deleting files when run with --fix, which is materially inconsistent with the declared read-only health-data access purpose.
The declared description is for a standalone, read-only Google Health API client. The supplied code chunk is instead a Python test script for a repository skill-sync gate. Its primary purpose, resources accessed, and behavior are entirely different: it creates temporary git repositories, copies scripts, edits and deletes local files, runs subprocesses, and verifies sync/fix behavior across harness directories. None of the described Google Health, OAuth2, read-only API, or JSON-emitting functionality is present. This is a clear description-behavior mismatch.
The instruction to treat broad phrases like "can you...", "I want to...", and "help me..." as authorization triggers is overly permissive and can cause the skill to interpret ordinary conversational text as approval to act. In an agent setting, this increases the chance of unintended execution scope, especially when combined with the document's guidance to continue work independently and persist authorization across turns.
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
// persistingSource wraps a refreshing TokenSource and re-saves the token to disk
// whenever .Token() yields a newer one (an access-token rotation after a
// refresh). x/oauth2 handles refresh-token rotation internally; we only need to
// persist the result so the on-disk access token stays fresh between runs
// (GOAL.md §8 — ghealth doesn't do this; we should).
type persistingSource struct {
base oauth2.TokenSource
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
// persistingSource wraps a refreshing TokenSource and re-saves the token to disk
// whenever .Token() yields a newer one (an access-token rotation after a
// refresh). x/oauth2 handles refresh-token rotation internally; we only need to
// persist the result so the on-disk access token stays fresh between runs
// (GOAL.md §8 — ghealth doesn't do this; we should).
type persistingSource struct {
base oauth2.TokenSource
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
// persistingSource wraps a refreshing TokenSource and re-saves the token to disk
// whenever .Token() yields a newer one (an access-token rotation after a
// refresh). x/oauth2 handles refresh-token rotation internally; we only need to
// persist the result so the on-disk access token stays fresh between runs
// (GOAL.md §8 — ghealth doesn't do this; we should).
type persistingSource struct {
base oauth2.TokenSource
The guide tells users to place an OAuth client ID and client secret into config.json or environment variables but does not explicitly warn that these values are sensitive and must be protected from source control, logs, backups, and shared directories. Even though this is a personal desktop OAuth client and not a high-privilege server credential, disclosure could let another party impersonate the app configuration or abuse the OAuth setup in combination with user interaction or stolen refresh tokens.
The document tells the user to click through an 'unsafe' browser warning, which is a security-relevant action affecting account privacy, but it does not prominently warn that this is only acceptable because the project was created by the same user for their own account. Without a clear caution, readers may normalize bypassing similar warnings in less safe contexts.
Without declared permissions the skill's intent is opaque and cannot be validated.
These tests validate SaveToken writing token.json and enforcing file permissions, demonstrating intentional local file creation/modification. The manifest's absolute wording about doing no writing is misleading unless interpreted as only applying to remote API data, because the implementation clearly writes OAuth cache files locally.
The manifest frames the skill as a read-only Google Health API client and explicitly says it does no writing. This test covers DeleteToken, which removes a local cached OAuth token file, showing the implementation includes local state mutation beyond pure read-only data access.
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.
// DefaultScopes is the full set of read-only Google Health scopes the tool
// requests at login, so an authorized token can read every data type the API
// exposes. Read-only only — the tool never requests a write scope.
var DefaultScopes = []string{
"https://www.googleapis.com/auth/googlehealth.profile.readonly",
"https://www.googleapis.com/auth/googlehealth.settings.readonly",
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.
t.Helper()
path := filepath.Join(dir, "config.json")
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
t.Fatalf("write config: %v", err)
}
return path
}
Skill enumerates or reads other installed skills. Access to other skills' SKILL.md files or the skills directory reveals prompt instructions, capabilities, and secrets that should be invisible to peer skills.
SUBTREE = "skills"
# A reference to something INSIDE the harness's own skills tree, in either slash flavor — the
# skills are written on Windows and quote both `.claude/skills/verify-openloop/...` and
# `pwsh <repo>\.claude\skills\run-e2e\...`. A deeper path is required so that prose enumerating
# the three trees ("`.claude/skills/`, `.cursor/skills/`, `.codex/skills/`", which harness-sync's
# own SKILL.md does) is left alone: that is a list of all three, not a pointer at one.
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
since a new skill written into one harness is exactly what needs propagating.
"""
top = f".{harness}/{SUBTREE}"
out = subprocess.run(["git", "ls-files", "-z", "--cached", "--others", "--exclude-standard", top],
cwd=ROOT, check=True, capture_output=True, text=True).stdout
return {p[len(top) + 1:]: ROOT / p for p in out.split("\0") if p}
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
three harnesses (a brand-new skill and the copies a previous --fix wrote look identical to it),
and the caller reports that case rather than picking a winner from no evidence.
"""
default = subprocess.run(["git", "symbolic-ref", "refs/remotes/origin/HEAD"], cwd=ROOT,
capture_output=True, text=True).stdout.strip()
base = subprocess.run(["git", "merge-base", default or "origin/main", "HEAD"], cwd=ROOT,
capture_output=True, text=True).stdout.strip()
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
"""
default = subprocess.run(["git", "symbolic-ref", "refs/remotes/origin/HEAD"], cwd=ROOT,
capture_output=True, text=True).stdout.strip()
base = subprocess.run(["git", "merge-base", default or "origin/main", "HEAD"], cwd=ROOT,
capture_output=True, text=True).stdout.strip()
paths = [f".{h}/{SUBTREE}/{rel}" for h in HARNESSES for rel in rel_paths]
changed = set()
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
cmds = ([["git", "diff", "--name-only", base, "--", *paths]] if base else []) + \
[["git", "status", "--porcelain", "--", *paths]]
for cmd in cmds:
out = subprocess.run(cmd, cwd=ROOT, capture_output=True, text=True).stdout
for line in out.splitlines():
line = line[3:] if cmd[1] == "status" else line # strip porcelain XY prefix
for h in HARNESSES:
Skill enumerates or reads other installed skills. Access to other skills' SKILL.md files or the skills directory reveals prompt instructions, capabilities, and secrets that should be invisible to peer skills.
SCRIPT = Path(__file__).resolve().parent / "sync-harness-skills.py"
HARNESSES = ("claude", "cursor", "codex")
SKILL = "skills/demo/SKILL.md"
def build_repo(tmp):
No suspicious patterns detected.