Back to skill

Security audit

Post-Experience Rater

Security checks for vulnerabilities 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.

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
The code executes a command taken from the environment variable FULCRA_CLI_COMMAND after parsing it with shlex.split. Although it avoids shell=True, an attacker who can influence the process environment can redirect execution to an arbitrary binary and cause token theft or unexpected code execution in the agent context.

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
The request target is derived from ATTIO_API_BASE, which is taken directly from the environment without validation. In a skill context, that allows an attacker or compromised runtime to redirect authenticated requests carrying the Bearer token to an arbitrary host, causing credential exfiltration and unauthorized data access; this is especially dangerous because the client is used for broad CRM reads and writes.

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
The command prefix comes from FULCRA_CLI_COMMAND in the environment and is split into argv before execution. If an attacker can influence the process environment, they can replace the intended Fulcra CLI with an arbitrary program, causing unexpected code execution with the copied environment and the skill's privileges.

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
API_BASE is sourced from the environment and concatenated into outbound requests that include a Bearer token in the Authorization header. If an attacker can set FULCRA_API_BASE, the helper will send authenticated requests and metadata to an attacker-controlled endpoint, causing credential exfiltration and unauthorized data disclosure/modification.

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
Code accesses environment variables that may contain secrets (API keys, tokens). This is a common pattern for credential theft.

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
Code accesses environment variables that may contain secrets (API keys, tokens). This is a common pattern for credential theft.

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
Copying the full environment and passing it into a subprocess can expose unrelated secrets present in environment variables to whichever executable is launched via FULCRA_CLI_COMMAND. Combined with the environment-controlled command, this expands secret exposure beyond the single access token the helper actually needs.

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
Model output is used without validation or sanitization. Unvalidated output injected into downstream contexts (SQL, shell, HTML) enables injection attacks and arbitrary code execution.

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
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

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
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

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
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

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
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

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
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Static analysis

No suspicious patterns detected.