Back to skill

Security audit

Personal Concierge

Security checks across malware telemetry and agentic risk

Overview

The skill is presented as a concierge router, but it ships broad Fulcra and Attio clients that can read, write, and delete sensitive personal and CRM data beyond what the wrapper README discloses.

Review this before installing if you do not want a concierge wrapper to include direct Fulcra and Attio data clients. Only use it in an environment where Fulcra and Attio tokens, FULCRA_CLI_COMMAND, FULCRA_API_BASE, ATTIO_API_BASE, and FULCRA_CONCIERGE_HOME are controlled and trusted, and prefer dry-run or explicit confirmation for any CRM or annotation mutation.

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

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
88% 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
91% confidence
Finding
with urllib.request.urlopen(req, timeout=30) as resp:

Tainted flow: 'cmd' from os.environ.get (line 44, 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
96% confidence
Finding
proc = subprocess.run(cmd, env=env, text=True, capture_output=True, timeout=120)

Tainted flow: 'tmp' from os.environ.get (line 53, credential/environment) → open (file write)

Medium
Category
Data Flow
Content
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)
Confidence
78% confidence
Finding
with open(tmp, "w", encoding="utf-8") as fh:

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
96% confidence
Finding
with urllib.request.urlopen(req, timeout=30) as response:

Lp3

Medium
Category
MCP Least Privilege
Confidence
96% confidence
Finding
The skill advertises orchestration behavior but declares no permissions despite invoking shell execution and relying on network, environment, and file capabilities through the referenced scripts and shared concierge stack. In a privacy-sensitive concierge context, undeclared capabilities reduce transparency and bypass least-privilege review, increasing the chance of unnoticed access to secrets, local files, or external services.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The documented role is a simple router/orchestrator, but the detected behavior includes direct CRM/API operations, annotation mutation, event-history reads, secret loading/persistence, and shared bootstrap functionality. This mismatch is dangerous because users and reviewers may authorize a low-risk wrapper while it can directly manipulate sensitive relationship data, personal annotations, and stored secrets behind the scenes.

Context-Inappropriate Capability

Medium
Confidence
84% confidence
Finding
This helper exposes a generic delete_record capability that can remove arbitrary Attio records, and the comment notes deleting a person also removes attached notes. In the context of a concierge-wrapper skill whose stated role is orchestration, this is unnecessarily destructive functionality that broadens the blast radius if the wrapper or a sub-skill is misused or prompt-injected.

Context-Inappropriate Capability

Medium
Confidence
88% confidence
Finding
The update command accepts arbitrary object names and raw JSON values, enabling unrestricted modification of Attio records beyond the concierge-wrapper's declared orchestration purpose. In a skill ecosystem, this kind of generic write primitive is dangerous because any prompt injection or accidental misuse can be turned into broad unauthorized data tampering.

Context-Inappropriate Capability

Medium
Confidence
98% confidence
Finding
This library claims to provide shared read access to the Fulcra CLI, but the command is globally overridable through FULCRA_CLI_COMMAND, which defeats that trust boundary. In a skill-orchestration context, that means a compromised wrapper, launcher, or runtime environment could cause all concierge flows to execute an attacker-controlled program while appearing to perform normal data reads.

Description-Behavior Mismatch

High
Confidence
96% confidence
Finding
The manifest says the skill is the main entry point that routes requests and chains concierge sub-skills, degrading gracefully when pieces are missing. In contrast, this code directly creates tags, creates/updates/deletes annotations, records data to ingest endpoints, and queries recent records, making it a general Fulcra annotation client rather than just orchestration glue.

Vague Triggers

Medium
Confidence
88% confidence
Finding
The README advertises very broad natural-language triggers like "start my day" and "status check," which can overlap with ordinary user speech and cause this orchestrator skill to activate unexpectedly. In an orchestration skill, accidental invocation is more risky because it can route into multiple downstream skills, potentially causing unintended data access, API calls, or multi-step actions.

Vague Triggers

Medium
Confidence
86% confidence
Finding
The wrapper uses broad phrases such as 'be my concierge' or requests spanning multiple skills, which can cause the orchestration layer to activate when the user intended a narrower action. In this skill's context, accidental activation is more dangerous because it may trigger multiple downstream skills touching health, calendar, relationship, and CRM data in one flow.

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
60% 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

62/62 vendors flagged this skill as clean.

View on VirusTotal

Static analysis

No suspicious patterns detected.