Back to skill

Security audit

Post-Experience Rater

Security checks across malware telemetry and agentic risk

Overview

The rating workflow itself is understandable, but the package also ships unrelated high-impact helpers for CRM, health/calendar reads, and deletion of Fulcra or Attio records.

Install only if you are comfortable giving the skill access to your Fulcra-authenticated environment and with the bundled shared helpers being present. Prefer a narrowed version that contains only post_experience.py and the minimal Fulcra write helper, removes Attio and delete/admin commands, pins API hosts, and uses --dry-run or explicit confirmation before saving ratings that mention companions, locations, or sensitive personal context.

SkillSpector

By NVIDIA
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Output HandlingUnvalidated Output Injection, Cross-Context Output, Unbounded Output
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
Findings (23)

subprocess module call

Medium
Category
Dangerous Code Execution
Content
candidates = [[*shlex.split(command), "auth", "print-access-token"]]
    for cmd in candidates:
        try:
            token = subprocess.check_output(
                cmd,
                env=env,
                text=True,
Confidence
85% confidence
Finding
token = subprocess.check_output( cmd, env=env, text=True, stderr=subprocess.DEVNULL, timeout=45,

Tainted flow: 'req' from os.environ.get (line 52, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Content
headers["Content-Type"] = "application/json"
    req = urllib.request.Request(url, data=data, headers=headers, method=method)
    try:
        with urllib.request.urlopen(req, timeout=30) as resp:
            body = resp.read().decode() or "{}"
            return resp.status, json.loads(body)
    except urllib.error.HTTPError as exc:
Confidence
96% confidence
Finding
with urllib.request.urlopen(req, timeout=30) as resp:

Tainted flow: 'cmd' from os.environ.get (line 32, credential/environment) → subprocess.run (code execution)

Medium
Category
Data Flow
Content
env.setdefault("PYTHONUTF8", "1")
    cmd = [*shlex.split(CLI_COMMAND, posix=(os.name != "nt")), *args]
    try:
        proc = subprocess.run(cmd, env=env, text=True, capture_output=True, timeout=120)
    except (FileNotFoundError, subprocess.TimeoutExpired) as exc:
        return 1, f"CLI invocation failed: {exc}"
    if proc.returncode != 0:
Confidence
94% confidence
Finding
proc = subprocess.run(cmd, env=env, text=True, capture_output=True, timeout=120)

Tainted flow: 'req' from os.environ.get (line 82, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Content
headers["Content-Type"] = "application/json"
    req = urllib.request.Request(API_BASE + path, data=data, headers=headers, method=method)
    try:
        with urllib.request.urlopen(req, timeout=30) as response:
            body = response.read()
            return response.status, body.decode() if body else ""
    except urllib.error.HTTPError as exc:
Confidence
97% confidence
Finding
with urllib.request.urlopen(req, timeout=30) as response:

Lp3

Medium
Category
MCP Least Privilege
Confidence
89% confidence
Finding
The skill instructs the agent to execute a local Python script, write a JSON payload to /tmp, rely on authenticated Fulcra CLI state, and perform a save operation that likely uses network access, but it declares no permissions. This mismatch is dangerous because it hides the skill's true capabilities from any permission model or reviewer, increasing the chance of unintended file, shell, environment, or network access being granted implicitly.

Context-Inappropriate Capability

Medium
Confidence
92% confidence
Finding
This file implements a generic Attio client with object listing, arbitrary record queries, create/assert/update/delete, and note operations, far beyond what a post-experience rating skill needs. Excess capability increases blast radius: prompt misuse, skill compromise, or accidental invocation could mutate unrelated CRM objects and expose unrelated records.

Context-Inappropriate Capability

High
Confidence
98% confidence
Finding
The skill includes a record deletion primitive even though lightweight rating capture does not require deletion. If exposed through the agent or accidentally triggered, it can irreversibly remove CRM records and associated notes, causing integrity loss and possible destruction of user history.

Description-Behavior Mismatch

Medium
Confidence
90% confidence
Finding
This shared helper exposes calendar, sleep, and generic health-data retrieval even though the post-experience rating skill is described as a quick preference/rating logger. That creates unnecessary access to sensitive personal data and broadens the blast radius if the skill is misused, compromised, or invoked in the wrong context.

Context-Inappropriate Capability

Medium
Confidence
91% confidence
Finding
The generic metric_samples function permits access to arbitrary numeric health metrics through get-records, which is not justified by a lightweight post-experience rating workflow. This increases privacy exposure because the same skill library can query sensitive biometrics unrelated to the user's request.

Description-Behavior Mismatch

Medium
Confidence
94% confidence
Finding
This helper exposes broad CRUD functionality over Fulcra annotations, including list, create, update, delete, record, and recent-read operations, while the skill is only supposed to capture a quick post-experience rating. Excess capability increases blast radius: if the skill is misused, prompt-injected, or called incorrectly, it can alter unrelated user data beyond its stated purpose.

Context-Inappropriate Capability

High
Confidence
98% confidence
Finding
The delete command allows permanent or logical removal of arbitrary annotations by ID, which is not necessary for logging a quick rating. In the context of an agent skill, unnecessary destructive actions are especially risky because accidental invocation, prompt injection, or tool misuse can erase user configuration/state.

Context-Inappropriate Capability

Medium
Confidence
93% confidence
Finding
The code can create and update arbitrary annotation definitions and tags, not just record a rating against an existing safe schema. That exceeds the skill's purpose and enables unintended schema sprawl, data integrity issues, and modification of unrelated user annotations if the tool is steered off-task.

Vague Triggers

Medium
Confidence
84% confidence
Finding
The README explicitly encourages broad natural-language invocation such as "rate that dinner," which can match casual conversation and cause the skill to run when the user did not intend to create a persistent record. In this skill’s context, unintended invocation is more concerning because it writes structured personal preference data to Fulcra, potentially storing sensitive social, location, or taste information without clear confirmation.

Missing User Warnings

Medium
Confidence
72% confidence
Finding
These functions retrieve calendar and sleep data through external CLI calls, and in the context of this specific skill that data is unrelated and sensitive. While the absence of disclosure is partly a UX/policy issue, coupling undisclosed access to unnecessary sensitive reads makes the behavior materially risky from a privacy standpoint.

Env Variable Harvesting

High
Category
Data Exfiltration
Content
def run_cli(args: list[str]) -> tuple[int, str]:
    env = os.environ.copy()
    env.setdefault("PYTHONUTF8", "1")
    cmd = [*shlex.split(CLI_COMMAND, posix=(os.name != "nt")), *args]
    try:
Confidence
60% confidence
Finding
os.environ.copy()

Env Variable Harvesting

High
Category
Data Exfiltration
Content
def access_token() -> str:
    env_token = os.environ.get("FULCRA_ACCESS_TOKEN")
    if env_token:
        return env_token.strip()
Confidence
70% confidence
Finding
os.environ.get("FULCRA_ACCESS_TOKEN

Env Variable Harvesting

High
Category
Data Exfiltration
Content
if env_token:
        return env_token.strip()

    env = os.environ.copy()
    env["HOME"] = DEFAULT_HOME

    command = os.environ.get("FULCRA_CLI_COMMAND", "uv tool run fulcra-api")
Confidence
82% confidence
Finding
os.environ.copy()

Unvalidated Output Injection

High
Category
Output Handling
Content
env.setdefault("PYTHONUTF8", "1")
    cmd = [*shlex.split(CLI_COMMAND, posix=(os.name != "nt")), *args]
    try:
        proc = subprocess.run(cmd, env=env, text=True, capture_output=True, timeout=120)
    except (FileNotFoundError, subprocess.TimeoutExpired) as exc:
        return 1, f"CLI invocation failed: {exc}"
    if proc.returncode != 0:
Confidence
95% confidence
Finding
subprocess.run(cmd, env=env, text=True, capture_output

Credential Access

High
Category
Privilege Escalation
Content
stdlib-only (urllib) so it runs under any `uv run`/python without extra installs,
mirroring the Fulcra annotation helper. It is BOTH a library (import the functions)
and a CLI (skills shell out to it). Every write supports --dry-run, and the token is
loaded via concierge_secrets (env ATTIO_API_KEY or ~/.fulcra-concierge/secrets.json) and
never printed.

Attio shapes used (verify against the live API; envelopes are the documented ones):
Confidence
70% confidence
Finding
secrets.json

Credential Access

High
Category
Privilege Escalation
Content
2. ~/.fulcra-concierge/secrets.json -- the persistent local store.

Secrets are never printed. Skills read keys through `get_secret`; they should
not open secrets.json themselves. Keeping one loader means one place to harden
(file perms, future keychain support) instead of N copies across skills.
"""
from __future__ import annotations
Confidence
70% confidence
Finding
secrets.json

Credential Access

High
Category
Privilege Escalation
Content
import os
from pathlib import Path

SECRETS_PATH = Path(os.environ.get("FULCRA_CONCIERGE_HOME", Path.home() / ".fulcra-concierge")) / "secrets.json"


def _load_file() -> dict:
Confidence
70% confidence
Finding
secrets.json

Credential Access

High
Category
Privilege Escalation
Content
def set_secret(name: str, value: str) -> None:
    """Persist a secret into secrets.json (merging with any existing keys) and
    lock the file down to the current user. Used by setup helpers, not skills."""
    SECRETS_PATH.parent.mkdir(parents=True, exist_ok=True)
    data = _load_file()
Confidence
70% confidence
Finding
secrets.json

Credential Access

High
Category
Privilege Escalation
Content
Secrets are never printed. Skills read keys through `get_secret`; they should
not open secrets.json themselves. Keeping one loader means one place to harden
(file perms, future keychain support) instead of N copies across skills.
"""
from __future__ import annotations
Confidence
70% confidence
Finding
keychain

VirusTotal

65/65 vendors flagged this skill as clean.

View on VirusTotal

Static analysis

No suspicious patterns detected.