Back to skill

Security audit

Post-Experience Rater

Security checks for vulnerabilities and agentic risk

Overview

The skill’s main rating workflow is understandable, but it ships broader credentialed tools and endpoint overrides that could expose or alter sensitive Fulcra and CRM data beyond the advertised quick rating use.

Review carefully before installing. Use this only in a trusted agent/runtime environment, pin the Fulcra CLI version, avoid setting custom API base environment variables, and consider removing or isolating the bundled Attio, health/calendar, historical-read, update, and delete utilities if you only want quick rating capture.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
Findings (4)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/lib/fulcra_annotations.py:24
Finding

Bearer Credentials Can Be Redirected to Environment-Controlled API Endpoints

Content
View full analysis
str: env_token = os.environ.get("FULCRA_ACCESS_TOKEN") 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") candidates = [[*shlex.split(command), "auth", "print-access-token"]] for cmd in candidates: try: token = subprocess.check_output( cmd, env=env, text=True, stderr=subprocess.DEVNULL, timeout=45, ).strip() except (FileNotFoundError, subprocess.CalledProcessError, subprocess.TimeoutExpired): continue if token: return token fail("No Fulcra token available. Set FULCRA_ACCESS_TOKEN or run Fulcra CLI auth login.") def request(method: str, path: str, payload: Any | None = None) -> tuple[int, str]: data = None headers = {"Authorization": f"Bearer {access_token()}"} if payload is not None: data = json.dumps(payload).encode() 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: body = exc.read().decode(errors="replace") return exc.code, body ``` ```python # scripts/lib/attio_client.py API_BASE = os.environ.get("ATTIO_API_BASE", "h ...[truncated 3190 chars]
Remediation
View remediation

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
scripts/lib/attio_client.py:62
Finding

Bundled Executable Utilities Expose Sensitive Read and Destructive Capabilities Beyond the Declared Skill Scope

Content
View full analysis
dict[str, Any]: """Today's (or any window's) calendar events: count + first start + titles.""" rc, body = run_cli(["calendar-events", start.isoformat(), end.isoformat()]) if rc != 0: return {"available": False, "reason": body[:300]} rows = [r for r in parse_records(body) if isinstance(r, dict)] titles = [r.get("title") or r.get("summary") or r.get("name") or "(untitled)" for r in rows] starts = [_field(r, "start_date", "start_time", "start") for r in rows] starts = [s for s in starts if s] return {"available": True, "event_count": len(rows), "titles": titles[:15], "first_start": min(starts) if starts else None} def sleep_summary(now: datetime | None = None, lookback_hours: int = 30) -> dict[str, Any]: now = now or datetime.now().astimezone() rc, body = run_cli(["get-records", "SleepStage", (now - timedelta(hours=lookback_hours)).isoformat(), now.isoformat()]) # Processing omitted here; the function returns sleep stages, bedtime, # wake time, efficiency, duration, and quality. def metric_samples(metric: str, start: datetime, end: datetime) -> dict[str, Any]: """Summary stats (avg/min/max/latest) for a numeric metric, e.g. HeartRate.""" rc, body = run ...[truncated 4752 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/lib/concierge_secrets.py:46
Finding

Predictable Secret Temporary File Is Permissioned Only After Plaintext Credentials Are Written

Content
View full analysis
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() data[name] = value # Write atomically-ish and restrict perms before writing the value. tmp = SECRETS_PATH.with_suffix(".tmp") with open(tmp, "w", encoding="utf-8") as fh: json.dump(data, fh, indent=2) try: os.chmod(tmp, 0o600) except OSError: pass os.replace(tmp, SECRETS_PATH) ``` ### Technical Analysis The temporary filename is deterministic: `secrets.tmp`. It is opened using normal file semantics without exclusive creation, symlink rejection, or a securely randomized name. Plaintext credentials are written before `chmod(0o600)` is attempted, so the file's initial access mode depends on the process umask and existing filesystem object. The comment states that permissions are restricted before writing, but the implementation does the opposite. The code also does not harden the parent directory or fail closed if `chmod` fails. In a shared or incorrectly permissioned filesystem context, another local user or process may observe the temporary file, pre-create it, or substitute a symbolic link. ### Attack Path 1. A local attacker identifies the configured concierge home directory and predictable `secrets.tmp` pathname. 2. The attacker monitors that location or pre-creates the temporary path as a symbolic link to a writable target. 3. A setup helper invokes `set_secret`. 4. Python opens the predictable path and writes the complete plaintext secret dictionary. 5. Before or despite the later `chmod`, the attacker reads the fi ...[truncated 767 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Note
Location
scripts/post_experience.py:45
Finding

Documented Rating Schema Is Not Enforced Before Persistent Storage

Content
View full analysis
dict: payload = json.loads(Path(args.payload).read_text(encoding="utf-8-sig")) recorded_at = payload.get("timestamp") or datetime.now(timezone.utc).isoformat() record = {k: payload.get(k) for k in KEEP if payload.get(k) is not None} if not record.get("name"): return {"ok": False, "error": "payload needs at least a 'name'"} res = cf.record_moment( name=ANNOTATION_NAME, description=ANNOTATION_DESC, tags=ANNOTATION_TAGS, payload=record, source=SOURCE, recorded_at=recorded_at, dry_run=args.dry_run, ) ``` ### Technical Analysis The Skill documents constrained input, including score values from 1 through 5 and specific field types. The implementation only verifies that `name` is truthy. It does not enforce: - score value types or the 1–5 range - the type of the `scores` object - allowed `experience_type` values - boolean type for `would_return` - array and string types for companions and tags - timestamp format or timezone awareness - per-field or total payload-size limits Because the accepted record is serialized and persisted, malformed, oversized, or semantically invalid data can enter the preference graph. A malformed timestamp can also cause failures when the verification code calls `datetime.fromisoformat`. ### Attack Path 1. An erroneous or untrusted producer creates a JSON payload with a non-empty `name`. 2. The payload includes out-of-range scores, incorrect field types, an invalid timestamp, or unusually large strings and arrays. 3. `cmd_save` accepts the payload because it performs no validation beyond `name`. 4. The record is serialized and submitted to Fulcra, or the invalid timestamp causes a failure during verification. 5. Downstream recommendation or analytics compon ...[truncated 538 chars]
Remediation
View remediation
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
Findings (36)

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

Critical
Category
Data Flow
Confidence
92% confidence
Finding

The request destination is derived from ATTIO_API_BASE, which is read from the environment and used without validation before sending an authenticated HTTP request. That allows an attacker who can influence the environment or runtime configuration to redirect the Bearer token and all CRM data to an arbitrary host, which is especially risky because this helper automatically attaches the Attio API credential to every request.

Content

Scanner excerpt · scripts/lib/attio_client.py (reported line 54)May include surrounding context.

python
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:

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

Critical
Category
Data Flow
Confidence
98% confidence
Finding

API_BASE is sourced from the environment and used to construct outbound requests that include the Authorization bearer token. If an attacker can set FULCRA_API_BASE, the script will send valid credentials and potentially sensitive annotation data to an attacker-controlled endpoint, creating a direct secret exfiltration path.

Content

Scanner excerpt · scripts/lib/fulcra_annotations.py (reported line 84)May include surrounding context.

python
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:

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding

Historical annotation/event readback over arbitrary date ranges is not necessary to capture a new rating and exposes a large corpus of personal behavioral data. In the context of a lightweight check-in skill, that mismatch makes inadvertent overcollection and privacy violations substantially more dangerous.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding

Historical annotation/event readback over arbitrary date ranges is not necessary to capture a new rating and exposes a large corpus of personal behavioral data. In the context of a lightweight check-in skill, that mismatch makes inadvertent overcollection and privacy violations substantially more dangerous.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding

Historical annotation/event readback over arbitrary date ranges is not necessary to capture a new rating and exposes a large corpus of personal behavioral data. In the context of a lightweight check-in skill, that mismatch makes inadvertent overcollection and privacy violations substantially more dangerous.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding

Historical annotation/event readback over arbitrary date ranges is not necessary to capture a new rating and exposes a large corpus of personal behavioral data. In the context of a lightweight check-in skill, that mismatch makes inadvertent overcollection and privacy violations substantially more dangerous.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding

Historical annotation/event readback over arbitrary date ranges is not necessary to capture a new rating and exposes a large corpus of personal behavioral data. In the context of a lightweight check-in skill, that mismatch makes inadvertent overcollection and privacy violations substantially more dangerous.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding

Historical annotation/event readback over arbitrary date ranges is not necessary to capture a new rating and exposes a large corpus of personal behavioral data. In the context of a lightweight check-in skill, that mismatch makes inadvertent overcollection and privacy violations substantially more dangerous.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

High
Category
Not specified by scanner
Confidence
99% confidence
Finding

The library provides an unrestricted delete_record operation and corresponding CLI command, enabling deletion of arbitrary Attio records by object slug and record ID. For a lightweight rating-capture skill, destructive delete access is unjustified and materially raises the risk of accidental or malicious data loss, including cascading note removal for person records.

Content

No source excerpt is available for this finding.

Env Variable Harvesting

High
Category
Data Exfiltration
Confidence
60% confidence
Finding

Code enumerates, copies, or searches environment variables for secrets. Bulk environment access can collect credentials unrelated to the skill's stated purpose.

Content

Scanner excerpt · scripts/lib/concierge_cli.py (reported line 30)May include surrounding context.

python
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:

Env Variable Harvesting

High
Category
Data Exfiltration
Confidence
60% confidence
Finding

Code enumerates, copies, or searches environment variables for secrets. Bulk environment access can collect credentials unrelated to the skill's stated purpose.

Content

Scanner excerpt · scripts/lib/fulcra_annotations.py (reported line 55)May include surrounding context.

python
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:

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/lib/attio_client.py (reported line 7)May include surrounding context.

python
Resolution order for any key:
  1. Environment variable (e.g. ATTIO_API_KEY) -- wins, good for CI / one-offs.
  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

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/lib/concierge_secrets.py (reported line 5)May include surrounding context.

python
Resolution order for any key:
  1. Environment variable (e.g. ATTIO_API_KEY) -- wins, good for CI / one-offs.
  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

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/lib/concierge_secrets.py (reported line 8)May include surrounding context.

python
Resolution order for any key:
  1. Environment variable (e.g. ATTIO_API_KEY) -- wins, good for CI / one-offs.
  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

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/lib/concierge_secrets.py (reported line 9)May include surrounding context.

python
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

Credential Access

High
Category
Privilege Escalation
Confidence
78% confidence
Finding

The code stores credentials in a plaintext JSON file under a path that can be redirected by the FULCRA_CONCIERGE_HOME environment variable. Although this is common in local tooling, plaintext secret storage increases exposure if the host is multi-user, backups are accessible, or the chosen directory has weak permissions.

Content

Scanner excerpt · scripts/lib/concierge_secrets.py (reported line 17)May include surrounding context.

python
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:

Credential Access

High
Category
Privilege Escalation
Confidence
84% confidence
Finding

This function persists secrets to secrets.json, which is plaintext at rest. The code attempts to chmod the temporary file but does not verify or correct permissions on the parent directory or final file, so credentials may be exposed through inherited umask, preexisting file permissions, or unsafe storage locations.

Content

Scanner excerpt · scripts/lib/concierge_secrets.py (reported line 47)May include surrounding context.

python
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()

Rp1

Medium
Category
MCP Rug Pull
Confidence
87% confidence
Finding

The README instructs users to run uv tool run fulcra-api without pinning a version, which can pull whatever package version is current at execution time. If the upstream package is compromised, maliciously updated, or unexpectedly changed, users may execute unreviewed code with access to their authenticated Fulcra context.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

The README suggests activating the skill with "Ask your agent ("rate that dinner — food 5, vibe 4, service 3")" without defining clear scope, constraints, or exclusion conditions. Phrases like this are close to ordinary conversational language and the file does not specify when the skill should or should not trigger.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding

The skill declares no explicit tool scope while instructing use of shell, filesystem, environment-backed auth, and networked CLI operations. In an agent setting, missing least-privilege boundaries increases the chance the skill can access or modify resources beyond what is needed for simple rating capture.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The activation examples are broad enough to trigger on ordinary conversation, which can cause accidental invocation and unintended collection/storage of personal preference data. In a skill that writes persistent annotations, ambiguous activation increases privacy and consent risk even if the author likely intended convenience.

Content

No source excerpt is available for this finding.

Rp1

Medium
Category
MCP Rug Pull
Confidence
83% confidence
Finding

The skill depends on uv tool run fulcra-api without pinning a specific version, which creates supply-chain and behavior-drift risk. A future or compromised release could change functionality, request broader access, or introduce unsafe behavior while the skill implicitly trusts it.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
Confidence
60% confidence
Finding

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.

Content

Scanner excerpt · scripts/lib/attio_client.py (reported line 6)May include surrounding context.

python
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.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

This helper exposes a wide set of CRM capabilities including listing objects, querying arbitrary records, creating/updating people, listing notes, and attaching notes, far beyond the stated purpose of capturing a quick post-experience rating. In a skill context, unnecessary capabilities increase blast radius: prompt misuse, command confusion, or compromised orchestration could leverage this library to read or modify unrelated CRM data.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

This shared helper exposes calendar and sleep/health data access that exceeds the stated purpose of a post-experience rating skill, which should only capture a quick subjective rating. Broad reusable access to unrelated sensitive domains increases the blast radius of misuse, prompt abuse, or accidental invocation and weakens data minimization.

Content

No source excerpt is available for this finding.