Back to skill

Security audit

Evening Debrief

Security checks across malware telemetry and agentic risk

Overview

The main debrief workflow is understandable, but the package includes broader account and CRM tools than an evening journal skill needs.

Review before installing. Use this only if you trust the publisher with Fulcra calendar/check-in data and stored reflections. Prefer removing or disabling the unused Attio and generic delete/update helpers, pinning the Fulcra API/CLI configuration to trusted values, and using least-privilege tokens.

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

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def fulcra_cli(args: list[str]) -> tuple[int, str]:
    base = os.environ.get("FULCRA_CLI_COMMAND", "uv tool run fulcra-api")
    try:
        proc = subprocess.run([*shlex.split(base), *args], capture_output=True, text=True, timeout=90)
    except (subprocess.TimeoutExpired, FileNotFoundError) as exc:
        return 1, f"{type(exc).__name__}: {exc}"
    return proc.returncode, proc.stdout if proc.returncode == 0 else proc.stdout + proc.stderr
Confidence
95% confidence
Finding
proc = subprocess.run([*shlex.split(base), *args], capture_output=True, text=True, timeout=90)

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
89% confidence
Finding
token = subprocess.check_output( cmd, env=env, text=True, stderr=subprocess.DEVNULL, timeout=45,

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

Medium
Category
Data Flow
Content
def fulcra_cli(args: list[str]) -> tuple[int, str]:
    base = os.environ.get("FULCRA_CLI_COMMAND", "uv tool run fulcra-api")
    try:
        proc = subprocess.run([*shlex.split(base), *args], capture_output=True, text=True, timeout=90)
    except (subprocess.TimeoutExpired, FileNotFoundError) as exc:
        return 1, f"{type(exc).__name__}: {exc}"
    return proc.returncode, proc.stdout if proc.returncode == 0 else proc.stdout + proc.stderr
Confidence
98% confidence
Finding
proc = subprocess.run([*shlex.split(base), *args], capture_output=True, text=True, timeout=90)

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

Lp3

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding
The skill invokes local scripts via shell, reads context from calendar/check-in sources, writes a JSON payload to /tmp, and uses a CLI/API backend, but it declares no permissions. That mismatch is dangerous because users and policy systems cannot accurately assess or constrain the skill's access to files, environment, shell execution, and networked data handling, increasing the chance of over-privileged or unintended data access.

Context-Inappropriate Capability

High
Confidence
95% confidence
Finding
This client exposes destructive and generic CRM capabilities, including record deletion, that are not needed for an evening debrief skill that should only store structured annotations. In a skill ecosystem, broad reusable primitives materially increase blast radius: if the skill is invoked unexpectedly, misrouted, prompt-injected, or reused by another component, it can delete unrelated CRM records and attached notes.

Context-Inappropriate Capability

Medium
Confidence
89% confidence
Finding
The module supports broad CRM discovery across objects, people, companies, records, and notes, which exceeds the stated need of an evening debrief skill. That mismatch makes the skill context more dangerous because a debrief tool handling reflective user content does not need broad read access to CRM data, so any misuse, prompt injection, or accidental invocation could expose unrelated personal or business information.

Vague Triggers

Medium
Confidence
86% confidence
Finding
The README suggests a very broad natural-language trigger (e.g. 'let's debrief my day') without clearly constraining when the skill should activate versus when a different skill or no skill should be used. In an agentic system, broad invocation language can cause accidental or premature execution, leading to unnecessary access to calendar/check-in context and unintended writes to Fulcra.

Vague Triggers

Medium
Confidence
84% confidence
Finding
The trigger phrases include broad, everyday language such as 'how was my day' and 'let's debrief,' plus an ambiguous condition about the user 'signals they're winding down.' This can cause unintended activation in normal conversation, leading the skill to read personal calendar/check-in context and store reflective personal data when the user may not have intended to launch this workflow.

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

65/65 vendors flagged this skill as clean.

View on VirusTotal

Static analysis

No suspicious patterns detected.