Back to skill

Security audit

Morning Briefing

Security checks across malware telemetry and agentic risk

Overview

The morning briefing workflow is mostly coherent, but it ships broader Fulcra and Attio write/delete tools than a read-only briefing skill needs.

Review before installing. This is not malicious on the evidence provided, but install it only if you trust the publisher with sensitive Fulcra data and are comfortable with the package including extra write-capable CRM/Fulcra utilities. Avoid untrusted FULCRA_API_BASE, ATTIO_API_BASE, FULCRA_CLI_COMMAND, or FULCRA_CONCIERGE_HOME settings, and treat the collector output as private.

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

Lp3

Medium
Category
MCP Least Privilege
Confidence
97% confidence
Finding
The skill instructs the agent to run a local Python script and make external requests while having no declared permissions, creating a capability/permission mismatch. In an agent environment, undeclared shell, network, environment, and file-write access reduces oversight and can enable sensitive data exposure or unintended system interaction, especially because the skill handles intimate health and calendar data.

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
The documented behavior omits collection of heart-rate data and references bundled shared code with broader CRM/Fulcra read-write capabilities than the skill's stated purpose. This over-collection and hidden capability expansion is dangerous because operators may authorize the skill for a narrow morning briefing use case while it can access or modify additional sensitive relationship and annotation data outside user expectations.

Context-Inappropriate Capability

High
Confidence
97% confidence
Finding
This client exposes create, assert, update, add-note, and delete capabilities even though the morning-briefing skill is described as a read-oriented briefing workflow. Excess write/delete functionality broadens the blast radius: any prompt-injection, misuse by another component, or accidental invocation could modify or delete CRM records unrelated to the skill's purpose.

Context-Inappropriate Capability

Medium
Confidence
95% confidence
Finding
This shared helper creates annotation definitions and writes moment records to Fulcra, which exceeds the stated read-only purpose of the morning-briefing skill. In a skill that should only assemble a briefing from existing sleep, calendar, check-in, and debrief data, bundled write primitives increase the chance of accidental or unauthorized state changes if the helper is imported or invoked incorrectly.

Context-Inappropriate Capability

High
Confidence
98% confidence
Finding
This helper supports creating, updating, deleting, and recording arbitrary Fulcra annotations, which materially exceeds the morning-briefing skill's stated read-and-summarize purpose. In an agent setting, this overbroad capability increases the blast radius of prompt mistakes, tool misuse, or malicious chaining by allowing state mutation where only read access should be necessary.

Description-Behavior Mismatch

High
Confidence
97% confidence
Finding
The file implements commands that mutate annotation definitions and data, conflicting with the manifest's description of delivering a morning briefing from existing sources. This mismatch is dangerous because users and orchestrators may grant the skill broader trust than warranted, while the code can alter or delete persisted user data.

Vague Triggers

Medium
Confidence
86% confidence
Finding
The trigger phrases include broad natural-language requests like 'brief me' and 'what do I have today,' which can cause accidental activation in ordinary conversation. In this skill's context, unintended activation is more concerning because it may pull sensitive sleep, mood, calendar, and weather data and present it when the user may not have intended to invoke a privacy-sensitive workflow.

Missing User Warnings

Medium
Confidence
88% confidence
Finding
This script aggregates and prints a single JSON blob containing highly sensitive personal data, including sleep metrics, calendar details, heart-rate data, subjective check-in content, and open action items. Even if intended for normal skill operation, emitting all of this to stdout without any in-code warning, minimization, or confirmation increases the risk of accidental exposure through logs, debugging output, shell history capture, agent transcripts, or downstream consumers that persist command output.

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
81% 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
79% 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.