Back to skill

Security audit

Morning Briefing

Security checks for vulnerabilities and agentic risk

Overview

The morning briefing workflow is mostly coherent, but the package also ships broad write-capable Fulcra and Attio helpers that exceed a read-oriented briefing skill.

Review before installing. Use it only in a private context with a trusted Fulcra CLI environment, do not set custom API base variables unless using trusted test endpoints, and consider removing or separating the Attio/Fulcra write helpers if you only need morning briefings.

Vulnerability Patterns
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (2)

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/lib/fulcra_annotations.py:24
Finding

Environment-Controlled Fulcra API Endpoint Can Receive Bearer Credentials

Content
View full analysis

Vulnerability Details

File Location: scripts/lib/fulcra_annotations.py:24, 76-84
Vulnerability Type: Unvalidated credential-bearing API destination
Risk Level: Medium

Vulnerable Code

python
API_BASE = os.environ.get(
    "FULCRA_API_BASE",
    "https://api.fulcradynamics.com"
).rstrip("/")

def request(method: str, path: str, payload: Any | None = None) -> tuple[int, str]:
    data = None
    headers = {"Authorization": f"Bearer {access_token()}"}
    if payload is not None:
        data = json.dumps(payload).encode()
        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 ""

Technical Analysis

The Fulcra client obtains its API base URL directly from the FULCRA_API_BASE environment variable. It does not verify that the resulting URL uses HTTPS or belongs to an approved Fulcra host before attaching the Fulcra bearer token.

An attacker who can influence the process environment can therefore redirect authenticated requests to an attacker-controlled origin. The affected request function is reachable from the primary workflow: scripts/collect.py invokes fulcra_read.read_annotation_events(), which invokes fulcra_annotations.request() to retrieve annotation definitions and events.

This issue requires the attacker to control the environment in which the skill runs. It does not independently grant an untrusted remote party that capability.

Attack Path

  1. The victim has a valid FULCRA_ACCESS_TOKEN or an authenticated Fulcra CLI session.
  2. An attacker or compromised launcher sets FULCRA_API_BASE to an attacker-controlled HTTP or HTTPS endpoint.
  3. The victim runs scripts/collect.py.
  4. The collection workflow calls `fu ...[truncated 773 chars]
Remediation
View remediation

Remediation Suggestions

  • Default to the fixed production endpoint and reject endpoint overrides in normal operation.
  • Parse the configured URL with urllib.parse.urlparse.
  • Require the https scheme.
  • Require an exact allowlisted hostname, such as api.fulcradynamics.com, rather than using suffix or substring checks.
  • Reject URLs containing user information, unexpected ports, fragments, or other ambiguous components.
  • If custom endpoints are necessary for development, require a separate explicit opt-in flag and test credentials that cannot access production data.
  • Before adding an authorization header, verify that the final request origin is trusted.
  • Ensure redirects cannot forward authorization credentials to an unapproved origin.
  • Run the skill from a trusted launcher that supplies a sanitized environment.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/lib/attio_client.py:34
Finding

Environment-Controlled Attio API Endpoint Can Receive Bearer Credentials

Content
View full analysis

Vulnerability Details

File Location: scripts/lib/attio_client.py:34, 42-54
Vulnerability Type: Unvalidated credential-bearing API destination
Risk Level: Medium

Vulnerable Code

python
API_BASE = os.environ.get(
    "ATTIO_API_BASE",
    "https://api.attio.com"
).rstrip("/")

def _request(method: str, path: str, payload: Any | None = None,
             params: dict[str, str] | None = None) -> tuple[int, Any]:
    token = get_secret("ATTIO_API_KEY", required=True)
    url = API_BASE + path
    if params:
        url += "?" + urllib.parse.urlencode(params)
    data = json.dumps(payload).encode() if payload is not None else None
    headers = {"Authorization": f"Bearer {token}"}
    if data is not None:
        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)

Technical Analysis

The Attio client accepts the ATTIO_API_BASE environment variable as an arbitrary API origin and sends the Attio bearer credential to that origin without enforcing HTTPS or validating the hostname.

Any Attio operation using _request() is affected, including record queries, person and company lookups, record creation or updates, deletion, and note operations. This module is bundled with the project but is not invoked by the morning briefing collection path shown in scripts/collect.py; exploitation requires a caller to use the Attio client.

Attack Path

  1. The victim has an ATTIO_API_KEY in the environment or local concierge secret store.
  2. An attacker or compromised launcher sets ATTIO_API_BASE to an attacker-controlled endpoint.
  3. The victim or another bundled workflow invokes an Attio client operation.
  4. _request() loads the Attio API key.
  5. It constructs the request using the attacker-controll ...[truncated 646 chars]
Remediation
View remediation

Remediation Suggestions

  • Remove arbitrary endpoint overrides from production builds where they are unnecessary.
  • Parse and validate the configured endpoint before loading or attaching the API key.
  • Require HTTPS and the exact approved hostname api.attio.com.
  • Reject embedded credentials, fragments, unexpected ports, and malformed URLs.
  • Use separate non-production credentials for explicitly enabled development endpoints.
  • Validate the destination again after redirects, or disable cross-origin redirects for authenticated requests.
  • Apply least-privilege scopes to Attio API keys and rotate any credential that may have been exposed.
  • Launch the client with a sanitized, trusted environment.
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
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
Findings (38)

Tainted flow: 'req' from os.environ.get (line 52, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
95% confidence
Finding

The request destination is derived from ATTIO_API_BASE, which is read from the environment and used to build the outbound URL without validation. In a skill environment, this allows an attacker who can influence environment/configuration to redirect authenticated requests, including the Bearer token, to an attacker-controlled host, creating a credential exfiltration/SSRF-style risk.

Content

Scanner excerpt · scripts/lib/attio_client.py (reported line 54)May include surrounding context.

python
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:

Tainted flow: 'req' from os.environ.get (line 82, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
98% confidence
Finding

API_BASE is taken from the environment and concatenated into outbound requests that include a Bearer token from access_token(). If an attacker can set FULCRA_API_BASE, the script will send authenticated requests and the Authorization header to an arbitrary host, enabling credential exfiltration and unauthorized access. This is especially dangerous because the file supports write operations, not just reads.

Content

Scanner excerpt · scripts/lib/fulcra_annotations.py (reported line 84)May include surrounding context.

python
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:

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding

The code is broadly aligned with the morning-briefing support workflow: it gathers sleep, calendar, weather, subjective state, and evening open loops, matching most of the declared inputs. However, there are material differences. First, it collects heart-rate data, which is an undeclared capability involving an additional health metric/resource. Second, the subjective source is specifically 'Morning Check-In', while the description says the skill consumes subjective-checkin / latest subjective check-in; that may indicate a different upstream source than declared. Third, the description frames the skill as delivering a personalized morning briefing adapted in tone and depth, but this code only aggregates inputs into JSON and explicitly leaves briefing composition to the agent. While helper/data-collection code can support a higher-level skill, the extra heart-rate access and the narrower actual behavior make this a description/behavior mismatch overall.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding

The declared purpose describes a user-facing morning briefing skill that aggregates sleep, calendar, weather, subjective check-in, and action items. The supplied code does not perform any of those functions. Instead, it is a generic bootstrap helper that searches local, repository, environment-variable, and home-directory paths for a shared library and modifies sys.path so other modules can be imported. This is materially different from the declared primary purpose. While bootstrap code can be a supporting detail, this chunk by itself exhibits none of the described skill behavior and instead performs unrelated environment/filesystem setup.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding

The code does not implement a morning briefing workflow or any logic related to sleep, calendar, weather, subjective check-ins, tone adaptation, or evening debrief action items. Instead, it is a standalone generic Attio API client that reads and writes CRM-style data, including record creation, updating, deletion, upsert, and note management. This is a materially different primary purpose and introduces undeclared data access and mutation capabilities against Attio, which are unrelated to the declared briefing skill.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding

The declared description is for a user-facing morning briefing skill that combines objective and subjective data into a personalized summary. The supplied code does not implement that end behavior. Instead, it is a support module containing generic Fulcra CLI accessors for calendar events, sleep summaries, and arbitrary metric samples. It accesses Fulcra data consistent with part of the description (sleep and calendar), but it lacks any logic for subjective check-ins, evening debrief action items, user-trigger handling, or briefing composition/adaptive tone. It also adds a broader undeclared capability: generic metric retrieval/statistics for any metric. This is a material description-behavior mismatch rather than a mere supporting detail, because the code chunk's primary purpose is a shared data-access library, not the described morning-briefing skill.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding

The declared description is for a user-facing morning briefing skill that reads multiple data sources (sleep, calendar, weather, check-ins, evening debrief items) and synthesizes them into a personalized briefing. The supplied code does none of that. Instead, it is a generic backend library for writing moment annotations into Fulcra, including creating annotation definitions, posting records, and verifying successful ingestion. This is a materially different primary purpose and includes undeclared write/persistence capabilities. No evidence in the code supports the described briefing behavior, data aggregation, tone adaptation, or trigger handling.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding

The declared description is for an end-user-facing morning briefing skill that combines multiple personal data sources and adapts output tone. The supplied code does none of that. Instead, it is a generic support library for credential management, handling secret retrieval from environment variables and a local file, persisting secrets, and checking for their presence. This is a materially different primary purpose and involves filesystem and secret-handling capabilities not described in the skill’s declared purpose.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding

The declared description is for a user-facing morning-briefing skill that aggregates multiple data sources (sleep, calendar, weather, subjective check-in, evening debrief items) and produces a personalized summary. The supplied code does none of that. Instead, it is a generic infrastructure utility for interacting with Fulcra annotation APIs, including full CRUD operations on annotations and recording/retrieving annotation data. This is a materially different primary purpose and includes significant undeclared write capabilities to Fulcra. There is no logic for morning briefings, no calendar/weather integration, no subjective tone adaptation, and no use of evening debrief items.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding

The declared description is for a user-facing morning-briefing skill that synthesizes multiple data sources and adapts output tone based on subjective state. The actual code chunk is instead a low-level library/CLI for reading Fulcra custom annotation events. While such a reader could be a supporting component for accessing pieces of briefing data, this chunk by itself does not implement the declared primary purpose and exposes broader generic capabilities (read any named annotation, list annotation names) not reflected in the description. There is no weather access, calendar summarization, sleep analysis, subjective check-in consumption logic, tone adaptation, or morning-briefing output generation in the provided code.

Content

No source excerpt is available for this finding.

Context Leakage

High
Category
Data Exfiltration
Confidence
93% confidence
Finding

This skill aggregates intimate data sources—sleep, heart rate, calendar, subjective check-ins, and evening debrief open loops—into one workflow. Even though the privacy section warns against oversharing, the context collection step creates a real concentration-of-sensitive-data risk: if invoked in the wrong context, logged verbosely, or surfaced to the wrong audience, it could disclose health, schedule, and emotional-state information together, making the impact substantially higher than a typical summarization skill.

Content

Scanner excerpt · SKILL.md (reported line 17)May include surrounding context.

  • Fulcra CLI authenticated (uv tool run fulcra-api auth login).
  • Run with uv run --python 3.12.

Step 1 -- Collect context

bash
uv run --python 3.12 ~/.claude/skills/morning-briefing/scripts/collect.py --location "New+York"

Description-Behavior Mismatch

High
Category
Not specified by scanner
Confidence
97% confidence
Finding

The file header and documented API shapes show this component is a general-purpose Attio client and CLI, not a narrowly scoped morning-briefing dependency. That mismatch with the manifest is dangerous because hidden external-management capabilities can be invoked unexpectedly in an agent context, bypassing user expectations and expanding the set of sensitive systems the skill can touch.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

High
Category
Not specified by scanner
Confidence
98% confidence
Finding

This helper exposes broad create, update, note, and delete operations against Attio even though the morning-briefing skill is described as a read-oriented briefing tool. That capability expansion materially increases blast radius: if the skill or wrapper is misused, prompted into invoking the CLI, or compromised, it can modify or destroy CRM data unrelated to the user-facing purpose.

Content

No source excerpt is available for this finding.

Env Variable Harvesting

High
Category
Data Exfiltration
Confidence
60% confidence
Finding

Code enumerates, copies, or searches environment variables for secrets. Bulk environment access can collect credentials unrelated to the skill's stated purpose.

Content

Scanner excerpt · scripts/lib/concierge_cli.py (reported line 30)May include surrounding context.

python
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:

Env Variable Harvesting

High
Category
Data Exfiltration
Confidence
60% confidence
Finding

Code enumerates, copies, or searches environment variables for secrets. Bulk environment access can collect credentials unrelated to the skill's stated purpose.

Content

Scanner excerpt · scripts/lib/fulcra_annotations.py (reported line 55)May include surrounding context.

python
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:

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/lib/attio_client.py (reported line 7)May include surrounding context.

python
Resolution order for any key:
  1. Environment variable (e.g. ATTIO_API_KEY) -- wins, good for CI / one-offs.
  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

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/lib/concierge_secrets.py (reported line 5)May include surrounding context.

python
Resolution order for any key:
  1. Environment variable (e.g. ATTIO_API_KEY) -- wins, good for CI / one-offs.
  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

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/lib/concierge_secrets.py (reported line 8)May include surrounding context.

python
Resolution order for any key:
  1. Environment variable (e.g. ATTIO_API_KEY) -- wins, good for CI / one-offs.
  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

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/lib/concierge_secrets.py (reported line 9)May include surrounding context.

python
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

Credential Access

High
Category
Privilege Escalation
Confidence
79% confidence
Finding

The module stores long-lived secrets in a plaintext JSON file under a path partially controlled by FULCRA_CONCIERGE_HOME. Although this is legitimate secret management functionality, plaintext local storage increases exposure if the filesystem is compromised, backups are broad, or the path is redirected to a less protected location.

Content

Scanner excerpt · scripts/lib/concierge_secrets.py (reported line 17)May include surrounding context.

python
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:

Credential Access

High
Category
Privilege Escalation
Confidence
84% confidence
Finding

set_secret persists credentials to secrets.json, which creates a concentrated local secret store in plaintext. Even though the code attempts to chmod the temp file to 0600, the parent directory permissions are not enforced and secrets may still be exposed through weak directory protections, inherited backups, or path manipulation via FULCRA_CONCIERGE_HOME.

Content

Scanner excerpt · scripts/lib/concierge_secrets.py (reported line 47)May include surrounding context.

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

Context-Inappropriate Capability

High
Category
Not specified by scanner
Confidence
98% confidence
Finding

The skill is described as a morning briefing tool that should read and summarize user data, yet this helper exposes broad create, update, delete, and record capabilities against Fulcra. That mismatch increases the blast radius substantially: if the skill or its wrapper is misused, prompted incorrectly, or compromised, it can mutate or destroy user data rather than merely read it. Excessive capability is a security issue in agent skills because prompts and tool selection are attack surfaces.

Content

No source excerpt is available for this finding.

Rp1

Medium
Category
MCP Rug Pull
Confidence
88% confidence
Finding

The README instructs users to run uv tool run fulcra-api without pinning a specific version or immutable source, which creates a supply-chain risk if a newer or compromised package is published under that name. Because this skill handles personal calendar, sleep, and check-in data, a malicious package update could exfiltrate sensitive user information or alter briefing behavior without the user noticing.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

The documented trigger phrase is broad enough that an agent may invoke the skill on loosely related requests, causing unnecessary access to sensitive data sources such as sleep records, calendar entries, and subjective check-ins. In this skill's context, overbroad activation is more dangerous because the aggregated briefing combines multiple private data domains, increasing the chance of unintended disclosure or collection.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
88% confidence
Finding

The skill invokes shell commands, accesses authenticated Fulcra tooling, performs network access, and may write files, but it declares no explicit tool scope or permission boundaries. In an agent setting, that increases the chance the skill is invoked with broader-than-necessary capabilities, enabling unintended data access or side effects if the script or its dependencies behave unexpectedly.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.