Back to skill

Security audit

Evening Debrief

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

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
The code executes an external command via subprocess.run using a command prefix taken from the FULCRA_CLI_COMMAND environment variable. Although shell=True is not used, this is still dangerous because an attacker who can influence the environment can redirect execution to an arbitrary binary or add unexpected arguments, resulting in arbitrary code execution in the agent context.

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
The code executes a command taken from the environment variable FULCRA_CLI_COMMAND after splitting it with shlex. While shell metacharacter injection is reduced because shell=False is used, an attacker who can influence the environment can cause execution of an arbitrary binary and obtain code execution in the agent context when the token fallback path runs.

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
There is a tainted flow from os.environ.get("FULCRA_CLI_COMMAND") into subprocess.run, enabling environment-controlled command selection. In an agent or automation environment, environment variables are often inherited from wrappers, CI, or orchestration layers, so this creates a realistic path to execute attacker-chosen programs with the skill's privileges.

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
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

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
The executable prefix comes from FULCRA_CLI_COMMAND, an environment-controlled value, and is split into argv before being passed to subprocess.run. Although this is not shell injection, it still permits execution of an attacker-selected binary or wrapper if the environment can be influenced, which is especially risky because the child inherits Fulcra auth context and can access sensitive calendar/health data.

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
API_BASE is derived from FULCRA_API_BASE and is used to build outbound authenticated HTTP requests that include the Bearer token from access_token(). If an attacker can set or influence that environment variable, the script will send the user's Fulcra access token and annotation data to an attacker-controlled server, creating a direct credential exfiltration path.

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
The helper copies the entire parent environment and passes it into the spawned process. If that environment contains credentials, API tokens, proxy settings, or other sensitive variables, any attacker-controlled replacement CLI selected via FULCRA_CLI_COMMAND would automatically receive them, increasing the blast radius from command substitution to credential disclosure and broader compromise.

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.