diff --git a/src/agents/skills/workspace.ts b/src/agents/skills/workspace.ts index 50f71d582bc..6b48a56ab60 100644 --- a/src/agents/skills/workspace.ts +++ b/src/agents/skills/workspace.ts @@ -20,6 +20,7 @@ import { } from "./frontmatter.js"; import { resolvePluginSkillDirs } from "./plugin-skills.js"; import { serializeByKey } from "./serialize.js"; +import { scanSource } from "../../security/skill-scanner.js"; import type { ParsedSkillFrontmatter, SkillEligibilityContext, @@ -391,6 +392,12 @@ function loadSkillEntries( let frontmatter: ParsedSkillFrontmatter = {}; try { const raw = fs.readFileSync(skill.filePath, "utf-8"); + // SECURITY: scan skill source for malicious patterns before loading + const findings = scanSource(raw, skill.filePath); + const critical = findings.filter((f) => f.severity === "critical"); + if (critical.length > 0) { + skillsLogger.warn(`[security] Skill "${skill.name}" has ${critical.length} critical finding(s): ${critical.map((f) => f.ruleId).join(", ")}`); + } frontmatter = parseFrontmatter(raw); } catch { // ignore malformed skills diff --git a/src/infra/heartbeat-runner.ts b/src/infra/heartbeat-runner.ts index 2d0bee48f0c..b18c8b919a2 100644 --- a/src/infra/heartbeat-runner.ts +++ b/src/infra/heartbeat-runner.ts @@ -18,6 +18,7 @@ import { } from "../auto-reply/heartbeat.js"; import { getReplyFromConfig } from "../auto-reply/reply.js"; import { HEARTBEAT_TOKEN } from "../auto-reply/tokens.js"; +import { detectSuspiciousPatterns } from "../security/external-content.js"; import type { ReplyPayload } from "../auto-reply/types.js"; import { getChannelPlugin } from "../channels/plugins/index.js"; import type { ChannelHeartbeatDeps } from "../channels/plugins/types.js"; @@ -535,6 +536,13 @@ async function resolveHeartbeatPreflight(params: { const heartbeatFilePath = path.join(workspaceDir, DEFAULT_HEARTBEAT_FILENAME); try { const heartbeatFileContent = await fs.readFile(heartbeatFilePath, "utf-8"); + // SECURITY: scan HEARTBEAT.md for injection patterns before injecting into agent prompt + const suspiciousPatterns = detectSuspiciousPatterns(heartbeatFileContent); + if (suspiciousPatterns.length > 0) { + log.warn( + `[security] Suspicious patterns detected in HEARTBEAT.md (patterns=${suspiciousPatterns.length}): ${suspiciousPatterns.slice(0, 3).join(", ")}`, + ); + } if (isHeartbeatContentEffectivelyEmpty(heartbeatFileContent)) { return { ...basePreflight,