Back to skill

Security audit

stalled-deal-nudge

Security checks for vulnerabilities and agentic risk

Overview

The skill is a disclosed weekly sales digest workflow that reads deal/activity data, posts to a locked Slack channel, and records a narrow dedupe ledger, with some residual prompt-injection risk from untrusted activity text.

Before installing, confirm the Slack channel is internal and appropriate for deal amounts and quoted prospect/customer text, set the channelId and owner map deliberately, and review any CRM-backed adaptation because it may extract broad CRM columns. Add prompt language or tests that treat activity subjects/bodies as untrusted data so emails or notes cannot steer the agent's Slack output or ledger writes.

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

T09 · Insecure Skill Coding Practices

Warning
Location
infra/agents/nudger.prompt.ts:46
Finding

Indirect Prompt Injection Through Untrusted CRM Activity Content

Content
View full analysis

Vulnerability Details

File Location: infra/agents/nudger.prompt.ts:46-53; privileged capabilities are configured in infra/agents/nudger.ts:47-61
Vulnerability Type: Indirect prompt injection into a tool-enabled agent
Risk Level: Medium

Vulnerable Code

infra/agents/nudger.prompt.ts:46-53:

ts
## 3. Research each remaining deal

Keep it short; this is a nudge, not an account brief.

1. The last activity: the most recent row in gtm_activities for the deal (subject and body). Quote the line that matters, with its date.
2. The workspace context: our positioning, known objections and competitors, so the reason and the draft speak to this deal.

Write, for each deal:

- **Why now**: one sentence, grounded in what you read: the close date that is about to slip, the next step nobody took, the objection left unanswered, or the fact that it has been flagged N Mondays running.
- **Draft**: a follow-up email of at most four sentences the owner could send, picking up from the quoted last activity. No placeholders in brackets; if a fact is missing, write around it. It is a draft for the owner, never sent by you.

infra/agents/nudger.ts:47-61:

ts
uses: [
  { ref: gtmOpportunities, readOnly: true },
  { ref: gtmAccounts, readOnly: true },
  { ref: gtmActivities, readOnly: true },
  { ref: dealNudges, readOnly: false },
  {
    ref: slack.actions.postMessage,
    config: {
      // PLACEHOLDER: the channel the digests land in, as a Slack id (C…)
      // read from the connector's channel autocomplete. Locked so a digest
      // that quotes deal amounts never lands in a customer shared channel.
      channelId: "C0123456789",
      format: "markdown",
      disableUnfurling: true,
    },
  },
],

Technical Analysis

The scheduled agent reads the subject and body of the latest activity and places their semantic content into the same model context used to deci ...[truncated 2862 chars]

Remediation
View remediation

Remediation Suggestions

  1. Explicitly classify all activity fields and workspace content as untrusted data in the system prompt. State that instructions, tool requests, policy claims, or role changes found in records must never be followed.
  2. Delimit retrieved fields using a structured representation such as JSON with fixed keys, and instruct the model to extract facts only from those fields.
  3. Separate content analysis from privileged actions. Use one restricted step to summarize activity text without Slack or write access, then pass only validated structured output to the posting step.
  4. Validate generated digests against an allowlisted schema before posting. Ensure every included deal ID belongs to the SQL-selected set and reject unexpected links, commands, mentions, or unrelated content.
  5. Enforce ledger integrity outside the model. Permit writes only for selected opportunity IDs, the computed ISO week, the corresponding owner, and a Slack timestamp returned by a successful post.
  6. Limit model reads to the minimum records and columns required for each selected deal rather than allowing unconstrained follow-up queries.
  7. Add adversarial tests containing prompt-injection strings in activity subjects and bodies, and verify that they cannot alter tool selection, expose unrelated records, change destinations, or create unauthorized ledger entries.
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (13)

Tp4

High
Category
MCP Tool Poisoning
Confidence
93% confidence
Finding

The supplied code does not implement the described skill behavior. It only defines an activity data model/schema. While activity data is a supporting component for a stalled-deal reporting skill, this chunk alone neither detects quiet deals nor generates summaries, drafts follow-ups, schedules a Monday workflow, or posts to Slack. The declared description presents a full end-user automation, but the actual code is limited to infrastructure for storing or reading activity records.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
94% confidence
Finding

The declared description is for a complete agent/workflow that queries deals and activities, determines which opportunities have gone quiet, drafts follow-ups, and posts a weekly Slack digest to each rep. The supplied code chunk does none of that; it only declares a native opportunity model with standard deal fields. Comments explicitly say this model does not decide what 'stalled' means and that such logic would live elsewhere (in agent SQL). Because the provided code's actual behavior is limited to schema/model definition and lacks the core described workflow capabilities, the description does not accurately represent this code chunk.

Content

No source excerpt is available for this finding.

Whitespace Padding

Medium
Category
Prompt Injection
Confidence
70% confidence
Finding

Large whitespace padding was detected (a block of blank lines or a long run of spaces). This can push injected instructions below or to the right of the visible area so a human reviewer never sees them while the agent still reads them. Manual review of the hidden content is recommended.

Content

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

md
**Derive before you ask.** An input with a lookup is looked up, not asked. Only the ones marked
_asked_ genuinely live in the operator's head.

| Input                                                     | Kind    | How it is answered                                                                                                                                                                    | Why it matters                                                                                                                                           |
| --------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| where deals and activities come from                       | asked   | the native models in this folder, filled by whatever the team already runs (a sync, call-capture, a sequencer), or a CRM via `crm-backed`. Check what fills them before anything else.                     | An empty `gtm_activities` model makes every open deal look quiet, and the first digest flags the whole pipeline.                                               |
| deal and activity columns                                  | derived | Read the models' columns once. The native `defineDeal` schema carries `is_closed`, `owner_id`, `stage_name`, `close_date`; `gtm_activities` carries `opportunity_id` and `occurred_at`.                              | The selection rests on these. A CRM-backed swap renames them, and the prompt's SQL has to follow.                                                          |

Whitespace Padding

Medium
Category
Prompt Injection
Confidence
70% confidence
Finding

Large whitespace padding was detected (a block of blank lines or a long run of spaces). This can push injected instructions below or to the right of the visible area so a human reviewer never sees them while the agent still reads them. Manual review of the hidden content is recommended.

Content

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

md
**Derive before you ask.** An input with a lookup is looked up, not asked. Only the ones marked
_asked_ genuinely live in the operator's head.

| Input                                                     | Kind    | How it is answered                                                                                                                                                                    | Why it matters                                                                                                                                           |
| --------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| where deals and activities come from                       | asked   | the native models in this folder, filled by whatever the team already runs (a sync, call-capture, a sequencer), or a CRM via `crm-backed`. Check what fills them before anything else.                     | An empty `gtm_activities` model makes every open deal look quiet, and the first digest flags the whole pipeline.                                               |
| deal and activity columns                                  | derived | Read the models' columns once. The native `defineDeal` schema carries `is_closed`, `owner_id`, `stage_name`, `close_date`; `gtm_activities` carries `opportunity_id` and `occurred_at`.                              | The selection rests on these. A CRM-backed swap renames them, and the prompt's SQL has to follow.                                                          |

Whitespace Padding

Medium
Category
Prompt Injection
Confidence
70% confidence
Finding

Large whitespace padding was detected (a block of blank lines or a long run of spaces). This can push injected instructions below or to the right of the visible area so a human reviewer never sees them while the agent still reads them. Manual review of the hidden content is recommended.

Content

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

md
| where deals and activities come from                       | asked   | the native models in this folder, filled by whatever the team already runs (a sync, call-capture, a sequencer), or a CRM via `crm-backed`. Check what fills them before anything else.                     | An empty `gtm_activities` model makes every open deal look quiet, and the first digest flags the whole pipeline.                                               |
| deal and activity columns                                  | derived | Read the models' columns once. The native `defineDeal` schema carries `is_closed`, `owner_id`, `stage_name`, `close_date`; `gtm_activities` carries `opportunity_id` and `occurred_at`.                              | The selection rests on these. A CRM-backed swap renames them, and the prompt's SQL has to follow.                                                          |
| `QUIET_DAYS` (`infra/agents/nudger.prompt.ts`)            | asked   | the number of days without logged activity after which the team agrees a deal is stalled. Default fourteen.                                                                         | Too short and the digest is noise reps mute; too long and it reports deals already lost.                                                                 |
| `OWNERS` (`infra/agents/nudger.prompt.ts`)                | derived | `SELECT DISTINCT owner_id` over open deals, then the name the digest prints for each.                                                                                                 | An owner missing from the map lands under "Unassigned or unmapped owner", which nobody reads as theirs.                                                  |
| `channelId` (`infra/agents/nudger.ts`)                    | asked   | the Slack channel id (`C…`) the digests may land in, read from the connector's channel autocomplete. Invite the bot.                                                                 | Digests quote deal amounts and prospect l
...[truncated 25 chars]

Whitespace Padding

Medium
Category
Prompt Injection
Confidence
70% confidence
Finding

Large whitespace padding was detected (a block of blank lines or a long run of spaces). This can push injected instructions below or to the right of the visible area so a human reviewer never sees them while the agent still reads them. Manual review of the hidden content is recommended.

Content

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

md
| `QUIET_DAYS` (`infra/agents/nudger.prompt.ts`)            | asked   | the number of days without logged activity after which the team agrees a deal is stalled. Default fourteen.                                                                         | Too short and the digest is noise reps mute; too long and it reports deals already lost.                                                                 |
| `OWNERS` (`infra/agents/nudger.prompt.ts`)                | derived | `SELECT DISTINCT owner_id` over open deals, then the name the digest prints for each.                                                                                                 | An owner missing from the map lands under "Unassigned or unmapped owner", which nobody reads as theirs.                                                  |
| `channelId` (`infra/agents/nudger.ts`)                    | asked   | the Slack channel id (`C…`) the digests may land in, read from the connector's channel autocomplete. Invite the bot.                                                                 | Digests quote deal amounts and prospect lines. Locked so they never land in a customer shared channel.                                                   |
| LLM connector and model (`infra/connectors/anthropic.ts`) | value   | **derived**: `cargo-ai connection connector list`. `languageModel` is a placeholder to set.                                                                                           | It is what every Monday is billed against.                                                                                                               |

Checked before moving on, not after the deploy:

Whitespace Padding

Medium
Category
Prompt Injection
Confidence
70% confidence
Finding

Large whitespace padding was detected (a block of blank lines or a long run of spaces). This can push injected instructions below or to the right of the visible area so a human reviewer never sees them while the agent still reads them. Manual review of the hidden content is recommended.

Content

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

md
| `QUIET_DAYS` (`infra/agents/nudger.prompt.ts`)            | asked   | the number of days without logged activity after which the team agrees a deal is stalled. Default fourteen.                                                                         | Too short and the digest is noise reps mute; too long and it reports deals already lost.                                                                 |
| `OWNERS` (`infra/agents/nudger.prompt.ts`)                | derived | `SELECT DISTINCT owner_id` over open deals, then the name the digest prints for each.                                                                                                 | An owner missing from the map lands under "Unassigned or unmapped owner", which nobody reads as theirs.                                                  |
| `channelId` (`infra/agents/nudger.ts`)                    | asked   | the Slack channel id (`C…`) the digests may land in, read from the connector's channel autocomplete. Invite the bot.                                                                 | Digests quote deal amounts and prospect lines. Locked so they never land in a customer shared channel.                                                   |
| LLM connector and model (`infra/connectors/anthropic.ts`) | value   | **derived**: `cargo-ai connection connector list`. `languageModel` is a placeholder to set.                                                                                           | It is what every Monday is billed against.                                                                                                               |

Checked before moving on, not after the deploy:

Whitespace Padding

Medium
Category
Prompt Injection
Confidence
70% confidence
Finding

Large whitespace padding was detected (a block of blank lines or a long run of spaces). This can push injected instructions below or to the right of the visible area so a human reviewer never sees them while the agent still reads them. Manual review of the hidden content is recommended.

Content

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

md
## What you can change

| Variation        | When it is right                                                    | How                                                                                                                               | What it costs                                                                                                                            |
| ---------------- | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `quiet-by-stage` | Late-stage deals should be flagged sooner than early ones           | Replace the single `QUIET_DAYS` condition in §1 of the prompt with a `CASE` on `dealstage`                                         | The rule gets harder to read in one glance. Keep the table of stage to days in the prompt, not in someone's head.                        |
| `per-rep-dm`     | Reps want their own digest, not a shared channel                    | Replace the locked `channelId` with one `postMessage` use per rep                                                                  | One use per rep to maintain. Dropping the lock instead lets the agent pick the destination, which is how a digest reaches the wrong person. |

Whitespace Padding

Medium
Category
Prompt Injection
Confidence
70% confidence
Finding

Large whitespace padding was detected (a block of blank lines or a long run of spaces). This can push injected instructions below or to the right of the visible area so a human reviewer never sees them while the agent still reads them. Manual review of the hidden content is recommended.

Content

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

md
## What you can change

| Variation        | When it is right                                                    | How                                                                                                                               | What it costs                                                                                                                            |
| ---------------- | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `quiet-by-stage` | Late-stage deals should be flagged sooner than early ones           | Replace the single `QUIET_DAYS` condition in §1 of the prompt with a `CASE` on `dealstage`                                         | The rule gets harder to read in one glance. Keep the table of stage to days in the prompt, not in someone's head.                        |
| `per-rep-dm`     | Reps want their own digest, not a shared channel                    | Replace the locked `channelId` with one `postMessage` use per rep                                                                  | One use per rep to maintain. Dropping the lock instead lets the agent pick the destination, which is how a digest reaches the wrong person. |

Whitespace Padding

Medium
Category
Prompt Injection
Confidence
70% confidence
Finding

Large whitespace padding was detected (a block of blank lines or a long run of spaces). This can push injected instructions below or to the right of the visible area so a human reviewer never sees them while the agent still reads them. Manual review of the hidden content is recommended.

Content

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

md
| ---------------- | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `quiet-by-stage` | Late-stage deals should be flagged sooner than early ones           | Replace the single `QUIET_DAYS` condition in §1 of the prompt with a `CASE` on `dealstage`                                         | The rule gets harder to read in one glance. Keep the table of stage to days in the prompt, not in someone's head.                        |
| `per-rep-dm`     | Reps want their own digest, not a shared channel                    | Replace the locked `channelId` with one `postMessage` use per rep                                                                  | One use per rep to maintain. Dropping the lock instead lets the agent pick the destination, which is how a digest reaches the wrong person. |
| `min-amount`     | Small deals flood the digest                                        | Add `amount >= <floor>` to the §1 SQL                                                                                              | Small deals that matter (a land for a big expansion) go unflagged.                                                                       |
| `manager-rollup` | Leadership wants one view of every stalled deal                     | Add a final post to the same channel: one line per owner with their count and total amount                                         | One more post. It turns a nudge into a scoreboard, which changes how reps read the digest above it.                                       |
| `crm-backed`     | The deals already live in HubSpot, Salesforce or Attio                                     | Replace `gtm_opportunities` and `gtm_activities` with connector-back
...[truncated 26 chars]

Whitespace Padding

Medium
Category
Prompt Injection
Confidence
70% confidence
Finding

Large whitespace padding was detected (a block of blank lines or a long run of spaces). This can push injected instructions below or to the right of the visible area so a human reviewer never sees them while the agent still reads them. Manual review of the hidden content is recommended.

Content

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

md
| `min-amount`     | Small deals flood the digest                                        | Add `amount >= <floor>` to the §1 SQL                                                                                              | Small deals that matter (a land for a big expansion) go unflagged.                                                                       |
| `manager-rollup` | Leadership wants one view of every stalled deal                     | Add a final post to the same channel: one line per owner with their count and total amount                                         | One more post. It turns a nudge into a scoreboard, which changes how reps read the digest above it.                                       |
| `crm-backed`     | The deals already live in HubSpot, Salesforce or Attio                                     | Replace `gtm_opportunities` and `gtm_activities` with connector-backed models of the same slugs that extract every record and every column (`fetchRecords`, `columnSelectionMode: "all"`, no filter): HubSpot `deals` plus `notes`, `meetings`, `calls` and `emails`, or Salesforce `Opportunity` plus `Task` and `Event`. Last activity maps to the latest engagement timestamp (`hs_timestamp`, `ActivityDate`), or to the deal roll-up (`notes_last_updated`, `LastActivityDate`) if you extract only deals. Adapt the column names in §1 of the prompt; drop the no-connector assertion in the contract | One connector to authorize and a sync to schedule. A roll-up property is only as fresh as the CRM keeps it; engagement rows are what actually happened. |
| `no-drafts`      | Reps prefer to write their own follow-ups                           | Drop the Draft line from §3 and §4 of the prompt                                                                                   | The digest says what is stuck but not how to unstick it; the cheapest follow-up is the one already written.                              |

## What should not change

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

This code file instructs the agent to post per-owner digests to Slack, which includes deal names, stages, amounts, close dates, quoted activity text, and IDs. While Slack posting is part of the skill’s behavior, there is no visible confirmation prompt, disclosure message, or warning in the file that customer or sales data will be sent to Slack.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
83% confidence
Finding

The file tells the agent to append one row per deal to deal_nudges, storing identifiers, timestamps, owner IDs, deal names, and Slack message timestamps. Although this persistence is operationally relevant, the code provides no visible confirmation, warning, or disclosure that the skill records this metadata.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.