Skip to content
This repository was archived by the owner on Apr 17, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions mastracode/src/agents/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,29 @@ import { TOOL_NAME_OVERRIDES } from '../tool-names.js';
// We support multiple skill locations for compatibility:
// 1. Project-local: .mastracode/skills (project-specific mastracode skills)
// 2. Project-local: .claude/skills (Claude Code compatible skills)
// 3. Global: ~/.mastracode/skills (user-wide mastracode skills)
// 4. Global: ~/.claude/skills (user-wide Claude Code skills)
// 3. Project-local: .claude/commands (Claude Code slash commands used as skills)
// 4. Project-local: .agents/commands (Superset agents slash commands)
// 5. Global: ~/.mastracode/skills (user-wide mastracode skills)
// 6. Global: ~/.claude/skills (user-wide Claude Code skills)
// 7. Global: ~/.claude/commands (user-wide Claude Code slash commands)
// 8. Global: ~/.agents/commands (user-wide Superset agents slash commands)

const mastraCodeLocalSkillsPath = path.join(process.cwd(), '.mastracode', 'skills');

const claudeLocalSkillsPath = path.join(process.cwd(), '.claude', 'skills');

const claudeLocalCommandsPath = path.join(process.cwd(), '.claude', 'commands');
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Project-local command paths are computed from process CWD at module load, causing workspace skill/command discovery to ignore request-specific projectPath for non-CWD projects.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At mastracode/src/agents/workspace.ts, line 32:

<comment>Project-local command paths are computed from process CWD at module load, causing workspace skill/command discovery to ignore request-specific `projectPath` for non-CWD projects.</comment>

<file context>
@@ -18,17 +18,29 @@ import { TOOL_NAME_OVERRIDES } from '../tool-names.js';
 
 const claudeLocalSkillsPath = path.join(process.cwd(), '.claude', 'skills');
 
+const claudeLocalCommandsPath = path.join(process.cwd(), '.claude', 'commands');
+
+const agentsLocalCommandsPath = path.join(process.cwd(), '.agents', 'commands');
</file context>
Fix with Cubic

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Project-local command skill directories are resolved from process CWD instead of the active project path, so command skills may be missed when projectPath != process.cwd().

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At mastracode/src/agents/workspace.ts, line 32:

<comment>Project-local command skill directories are resolved from process CWD instead of the active project path, so command skills may be missed when projectPath != process.cwd().</comment>

<file context>
@@ -18,17 +18,29 @@ import { TOOL_NAME_OVERRIDES } from '../tool-names.js';
 
 const claudeLocalSkillsPath = path.join(process.cwd(), '.claude', 'skills');
 
+const claudeLocalCommandsPath = path.join(process.cwd(), '.claude', 'commands');
+
+const agentsLocalCommandsPath = path.join(process.cwd(), '.agents', 'commands');
</file context>
Fix with Cubic


const agentsLocalCommandsPath = path.join(process.cwd(), '.agents', 'commands');

const mastraCodeGlobalSkillsPath = path.join(os.homedir(), '.mastracode', 'skills');

const claudeGlobalSkillsPath = path.join(os.homedir(), '.claude', 'skills');

const claudeGlobalCommandsPath = path.join(os.homedir(), '.claude', 'commands');

const agentsGlobalCommandsPath = path.join(os.homedir(), '.agents', 'commands');

// Mastra's LocalSkillSource.readdir uses Node's Dirent.isDirectory() which
// returns false for symlinks. Tools like `npx skills add` install skills as
// symlinks, so we need to resolve them. For each symlinked skill directory,
Expand Down Expand Up @@ -77,8 +89,12 @@ function collectSkillPaths(skillsDirs: string[]): string[] {
export const skillPaths = collectSkillPaths([
mastraCodeLocalSkillsPath,
claudeLocalSkillsPath,
claudeLocalCommandsPath,
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Added commands paths are treated as skill roots, but core discovery only loads SKILL.md skills, so command markdown files are likely not discovered.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At mastracode/src/agents/workspace.ts, line 92:

<comment>Added commands paths are treated as skill roots, but core discovery only loads SKILL.md skills, so command markdown files are likely not discovered.</comment>

<file context>
@@ -77,8 +89,12 @@ function collectSkillPaths(skillsDirs: string[]): string[] {
 export const skillPaths = collectSkillPaths([
   mastraCodeLocalSkillsPath,
   claudeLocalSkillsPath,
+  claudeLocalCommandsPath,
+  agentsLocalCommandsPath,
   mastraCodeGlobalSkillsPath,
</file context>
Fix with Cubic

agentsLocalCommandsPath,
mastraCodeGlobalSkillsPath,
claudeGlobalSkillsPath,
claudeGlobalCommandsPath,
agentsGlobalCommandsPath,
]);

const WORKSPACE_ID_PREFIX = 'mastra-code-workspace';
Expand Down
19 changes: 17 additions & 2 deletions packages/core/src/harness/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1281,12 +1281,20 @@ export class Harness<TState = {}> {
tracingContext,
tracingOptions,
requestContext: requestContextInput,
preloadSkills,
}: {
content: string;
files?: Array<{ data: string; mediaType: string; filename?: string }>;
tracingContext?: TracingContext;
tracingOptions?: TracingOptions;
requestContext?: RequestContext;
/**
* Skill names to load before the LLM processes the user message.
* For each name, the agent will call the skill tool before responding,
* making the skill content available as context. The tool calls appear
* in the conversation UI immediately on submission.
*/
preloadSkills?: string[];
}): Promise<void> {
if (!this.currentThreadId) {
const thread = await this.createThread();
Expand Down Expand Up @@ -1316,7 +1324,14 @@ export class Harness<TState = {}> {

streamOptions.toolsets = await this.buildToolsets(requestContext);

let messageInput: string | Record<string, unknown> = content;
// Prepend skill loading instructions when the caller has requested pre-loading.
// The agent calls the skill tool for each name before responding, surfacing
// visible tool call blocks in the UI immediately on submission.
const effectiveContent = preloadSkills?.length
? `<system>The user has invoked the following skills. Before responding to their request, you MUST call the skill tool once for each skill listed here — do not skip or defer this step:\n${preloadSkills.map(s => ` - ${s}`).join('\n')}\n</system>\n\n${content}`
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Internal preload control instructions are injected into persisted user message content, causing prompt leakage and long-lived context pollution.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/src/harness/harness.ts, line 1331:

<comment>Internal preload control instructions are injected into persisted user message content, causing prompt leakage and long-lived context pollution.</comment>

<file context>
@@ -1316,7 +1324,14 @@ export class Harness<TState = {}> {
+      // The agent calls the skill tool for each name before responding, surfacing
+      // visible tool call blocks in the UI immediately on submission.
+      const effectiveContent = preloadSkills?.length
+        ? `<system>The user has invoked the following skills. Before responding to their request, you MUST call the skill tool once for each skill listed here — do not skip or defer this step:\n${preloadSkills.map(s => `  - ${s}`).join('\n')}\n</system>\n\n${content}`
+        : content;
+
</file context>
Fix with Cubic

: content;
Comment on lines +1327 to +1332
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check how preloadSkills values are populated upstream
# Look for callers of sendMessage that pass preloadSkills to understand the data source

rg -n --type=ts -C5 'preloadSkills' -g '!packages/core/src/harness/harness.ts'

Repository: superset-sh/mastra

Length of output: 44


🏁 Script executed:

# Find the function containing lines 1327-1332 in harness.ts
head -n 1332 packages/core/src/harness/harness.ts | tail -n 50

Repository: superset-sh/mastra

Length of output: 2243


🏁 Script executed:

# Search for the function signature that includes preloadSkills parameter
rg -n 'preloadSkills' packages/core/src/harness/harness.ts -B 20 | head -n 100

Repository: superset-sh/mastra

Length of output: 2611


🏁 Script executed:

# Check if there are any type definitions or interfaces that define preloadSkills
rg -n 'interface|type.*\{' packages/core/src/harness/harness.ts | head -n 20

Repository: superset-sh/mastra

Length of output: 1256


🏁 Script executed:

# Look for skill-related validation or normalization
rg -n 'skill.*validat|skill.*normaliz|validat.*skill' packages/core/src/harness/harness.ts

Repository: superset-sh/mastra

Length of output: 44


🏁 Script executed:

# Find all callers of sendMessage to see how preloadSkills is passed
rg -n 'sendMessage' packages/core --type=ts | head -n 50

Repository: superset-sh/mastra

Length of output: 1623


🏁 Script executed:

# Look for skill-related type definitions or constants that might constrain names
rg -n 'skill' packages/core/src --type=ts -i | grep -E 'interface|type|const.*=' | head -n 30

Repository: superset-sh/mastra

Length of output: 3668


🏁 Script executed:

# Check if there's any validation or constraints on tool/skill names
rg -n 'tool.*name|skill.*name|buildToolsets' packages/core/src/harness/harness.ts | head -n 20

Repository: superset-sh/mastra

Length of output: 1362


🏁 Script executed:

# Check the Skill type definition and how skill names are validated/constrained
rg -n 'interface Skill|type Skill' packages/core/src --type=ts -A 10 | head -n 50

Repository: superset-sh/mastra

Length of output: 3886


🏁 Script executed:

# Look for how skill names are used or validated in skill resolution
rg -n 'resolveByName|resolveByPath|Skill.*name' packages/core/src/workspace/skills --type=ts -A 3 | head -n 60

Repository: superset-sh/mastra

Length of output: 4460


🏁 Script executed:

# Check if preloadSkills parameter is documented with any constraints
rg -n 'preloadSkills' packages/core/src --type=ts -B 5 -A 5

Repository: superset-sh/mastra

Length of output: 3066


🏁 Script executed:

# Check if preloadSkills is validated anywhere in the sendMessage function body
sed -n '1278,1400p' packages/core/src/harness/harness.ts | grep -n -C 3 'preloadSkills'

Repository: superset-sh/mastra

Length of output: 1264


🏁 Script executed:

# Check if there's any validation or error handling for invalid skill names
rg -n 'preloadSkills' packages/core/src/harness/harness.ts -A 30 | grep -E 'validat|check|throw|error|sanitiz'

Repository: superset-sh/mastra

Length of output: 44


Validate preloadSkills names to prevent prompt injection.

The preloadSkills parameter accepts arbitrary strings that are directly interpolated into the system prompt without validation. While legitimate skill names follow a strict pattern (lowercase letters/numbers/hyphens, 1-64 chars, no leading/trailing/consecutive hyphens), the parameter bypasses this validation. An attacker able to influence these values could inject additional instructions into the LLM system prompt.

Apply the same validation used for skill metadata: either validate that each name matches the skill name pattern, or verify that each name exists in the workspace skill registry before interpolation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/harness/harness.ts` around lines 1327 - 1332, Validate and
sanitize preloadSkills before interpolating into effectiveContent: ensure each
entry in the preloadSkills array matches the canonical skill-name pattern
(lowercase letters/numbers and single hyphens, 1–64 chars, no
leading/trailing/consecutive hyphens) or, alternatively, confirm each name
exists in the workspace skill registry, then use only the validated list when
building effectiveContent; perform this check right before constructing
effectiveContent (referencing preloadSkills and effectiveContent) and either
filter out invalid names or throw/return an error so untrusted strings cannot be
injected into the system prompt.


let messageInput: string | Record<string, unknown> = effectiveContent;
if (files?.length) {
const fileParts = files.map(f => {
const isText = f.mediaType.startsWith('text/') || f.mediaType === 'application/json';
Expand All @@ -1343,7 +1358,7 @@ export class Harness<TState = {}> {
});
messageInput = {
role: 'user',
content: [{ type: 'text', text: content }, ...fileParts],
content: [{ type: 'text', text: effectiveContent }, ...fileParts],
};
}

Expand Down