Back to skill

Security audit

Personal Concierge

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

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
The script executes an external command derived from the FULCRA_CLI_COMMAND environment variable to obtain an access token. Although shlex.split avoids shell metacharacter expansion, this still allows an attacker who can influence the environment to redirect execution to an arbitrary binary and exfiltrate or misuse credentials. In an agent skill context, invoking subprocesses for auth retrieval increases the attack surface beyond simple orchestration.

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
The request destination is derived from ATTIO_API_BASE, which is sourced from an environment variable and used without validation before sending an authenticated HTTP request. An attacker who can influence the environment can redirect the client to an arbitrary host and capture the Bearer token or cause privileged requests to be sent to an unintended service, making this a real SSRF/credential-exfiltration risk.

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
The executable command is derived from FULCRA_CLI_COMMAND in the environment and then passed to subprocess.run, so anyone who can influence the process environment can redirect this code to run an arbitrary program. Because the full environment is also forwarded to the child, this can become arbitrary code execution plus exposure of sensitive tokens such as Fulcra credentials.

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
The write target is derived from SECRETS_PATH, which in turn is influenced by the FULCRA_CONCIERGE_HOME environment variable. If an attacker can control the environment in which setup helpers run, they can redirect secret writes to an unintended filesystem location, potentially overwriting files accessible to the current user or placing secrets in a location with weaker protections.

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
The request target is built from API_BASE, which is sourced from the environment, and every request includes a Bearer token from access_token(). If an attacker can set FULCRA_API_BASE, they can redirect authenticated requests to an attacker-controlled server and capture the token and any annotation data, creating a credential exfiltration and SSRF-style sink. This is especially risky because the helper performs both reads and writes with broad API access.

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

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.