diff --git a/src/features/claude-code-plugin-loader/loader.ts b/src/features/claude-code-plugin-loader/loader.ts
index 16771ad9..9e366eac 100644
--- a/src/features/claude-code-plugin-loader/loader.ts
+++ b/src/features/claude-code-plugin-loader/loader.ts
@@ -5,6 +5,7 @@ import type { AgentConfig } from "@opencode-ai/sdk"
import { parseFrontmatter } from "../../shared/frontmatter"
import { sanitizeModelField } from "../../shared/model-sanitizer"
import { isMarkdownFile, resolveSymlink } from "../../shared/file-utils"
+import { resolveSkillPathReferences } from "../../shared/skill-path-resolver"
import { log } from "../../shared/logger"
import { expandEnvVarsInObject } from "../claude-code-mcp-loader/env-expander"
import { transformMcpServer } from "../claude-code-mcp-loader/transformer"
@@ -297,11 +298,12 @@ export function loadPluginSkillsAsCommands(
const originalDescription = data.description || ""
const formattedDescription = `(plugin: ${plugin.name} - Skill) ${originalDescription}`
+ const resolvedBody = resolveSkillPathReferences(body.trim(), resolvedPath)
const wrappedTemplate = `
Base directory for this skill: ${resolvedPath}/
File references (@path) in this skill are relative to this directory.
-${body.trim()}
+${resolvedBody}
diff --git a/src/features/opencode-skill-loader/async-loader.ts b/src/features/opencode-skill-loader/async-loader.ts
index 54f04b8c..55148bca 100644
--- a/src/features/opencode-skill-loader/async-loader.ts
+++ b/src/features/opencode-skill-loader/async-loader.ts
@@ -5,6 +5,7 @@ import yaml from "js-yaml"
import { parseFrontmatter } from "../../shared/frontmatter"
import { sanitizeModelField } from "../../shared/model-sanitizer"
import { resolveSymlink, isMarkdownFile } from "../../shared/file-utils"
+import { resolveSkillPathReferences } from "../../shared/skill-path-resolver"
import type { CommandDefinition } from "../claude-code-command-loader/types"
import type { SkillScope, SkillMetadata, LoadedSkill } from "./types"
import type { SkillMcpConfig } from "../skill-mcp-manager/types"
@@ -90,11 +91,12 @@ export async function loadSkillFromPathAsync(
const isOpencodeSource = scope === "opencode" || scope === "opencode-project"
const formattedDescription = `(${scope} - Skill) ${originalDescription}`
+ const resolvedBody = resolveSkillPathReferences(body.trim(), resolvedPath)
const wrappedTemplate = `
Base directory for this skill: ${resolvedPath}/
File references (@path) in this skill are relative to this directory.
-${body.trim()}
+${resolvedBody}
diff --git a/src/features/opencode-skill-loader/loader.ts b/src/features/opencode-skill-loader/loader.ts
index 59522475..f3d22d57 100644
--- a/src/features/opencode-skill-loader/loader.ts
+++ b/src/features/opencode-skill-loader/loader.ts
@@ -4,6 +4,7 @@ import yaml from "js-yaml"
import { parseFrontmatter } from "../../shared/frontmatter"
import { sanitizeModelField } from "../../shared/model-sanitizer"
import { resolveSymlinkAsync, isMarkdownFile } from "../../shared/file-utils"
+import { resolveSkillPathReferences } from "../../shared/skill-path-resolver"
import { getClaudeConfigDir } from "../../shared"
import { getOpenCodeConfigDir } from "../../shared/opencode-config-dir"
import type { CommandDefinition } from "../claude-code-command-loader/types"
@@ -84,11 +85,12 @@ async function loadSkillFromPath(
const isOpencodeSource = scope === "opencode" || scope === "opencode-project"
const formattedDescription = `(${scope} - Skill) ${originalDescription}`
+ const resolvedBody = resolveSkillPathReferences(body.trim(), resolvedPath)
const templateContent = `
Base directory for this skill: ${resolvedPath}/
File references (@path) in this skill are relative to this directory.
-${body.trim()}
+${resolvedBody}
diff --git a/src/features/opencode-skill-loader/merger.ts b/src/features/opencode-skill-loader/merger.ts
index cace1a22..e40c2752 100644
--- a/src/features/opencode-skill-loader/merger.ts
+++ b/src/features/opencode-skill-loader/merger.ts
@@ -8,6 +8,7 @@ import { homedir } from "os"
import { parseFrontmatter } from "../../shared/frontmatter"
import { sanitizeModelField } from "../../shared/model-sanitizer"
import { deepMerge } from "../../shared/deep-merge"
+import { resolveSkillPathReferences } from "../../shared/skill-path-resolver"
function parseAllowedToolsFromMetadata(allowedTools: string | string[] | undefined): string[] | undefined {
if (!allowedTools) return undefined
@@ -105,11 +106,12 @@ function configEntryToLoaded(
const description = entry.description || fileMetadata.description || ""
const resolvedPath = entry.from ? dirname(resolveFilePath(entry.from, configDir)) : configDir || process.cwd()
+ const resolvedTemplate = resolveSkillPathReferences(template.trim(), resolvedPath)
const wrappedTemplate = `
Base directory for this skill: ${resolvedPath}/
File references (@path) in this skill are relative to this directory.
-${template.trim()}
+${resolvedTemplate}
diff --git a/src/shared/skill-path-resolver.test.ts b/src/shared/skill-path-resolver.test.ts
new file mode 100644
index 00000000..e0e3b854
--- /dev/null
+++ b/src/shared/skill-path-resolver.test.ts
@@ -0,0 +1,104 @@
+import { describe, it, expect } from "bun:test"
+import { resolveSkillPathReferences } from "./skill-path-resolver"
+
+describe("resolveSkillPathReferences", () => {
+ it("resolves @path references containing a slash to absolute paths", () => {
+ //#given
+ const content = "Run `python3 @scripts/search.py` to search"
+ const basePath = "/home/user/.config/opencode/skills/frontend-ui-ux"
+
+ //#when
+ const result = resolveSkillPathReferences(content, basePath)
+
+ //#then
+ expect(result).toBe(
+ "Run `python3 /home/user/.config/opencode/skills/frontend-ui-ux/scripts/search.py` to search"
+ )
+ })
+
+ it("resolves multiple @path references in the same content", () => {
+ //#given
+ const content = "Script: @scripts/search.py\nData: @data/styles.csv"
+ const basePath = "/skills/frontend"
+
+ //#when
+ const result = resolveSkillPathReferences(content, basePath)
+
+ //#then
+ expect(result).toBe(
+ "Script: /skills/frontend/scripts/search.py\nData: /skills/frontend/data/styles.csv"
+ )
+ })
+
+ it("resolves directory references with trailing slash", () => {
+ //#given
+ const content = "Data files: @data/"
+ const basePath = "/skills/frontend"
+
+ //#when
+ const result = resolveSkillPathReferences(content, basePath)
+
+ //#then
+ expect(result).toBe("Data files: /skills/frontend/data/")
+ })
+
+ it("does not resolve single-segment @references without slash", () => {
+ //#given
+ const content = "@param value @ts-ignore @path"
+ const basePath = "/skills/frontend"
+
+ //#when
+ const result = resolveSkillPathReferences(content, basePath)
+
+ //#then
+ expect(result).toBe("@param value @ts-ignore @path")
+ })
+
+ it("does not resolve email addresses", () => {
+ //#given
+ const content = "Contact user@example.com for help"
+ const basePath = "/skills/frontend"
+
+ //#when
+ const result = resolveSkillPathReferences(content, basePath)
+
+ //#then
+ expect(result).toBe("Contact user@example.com for help")
+ })
+
+ it("handles deeply nested path references", () => {
+ //#given
+ const content = "@data/stacks/html-tailwind.csv"
+ const basePath = "/skills/frontend"
+
+ //#when
+ const result = resolveSkillPathReferences(content, basePath)
+
+ //#then
+ expect(result).toBe("/skills/frontend/data/stacks/html-tailwind.csv")
+ })
+
+ it("returns content unchanged when no @path references exist", () => {
+ //#given
+ const content = "No path references here"
+ const basePath = "/skills/frontend"
+
+ //#when
+ const result = resolveSkillPathReferences(content, basePath)
+
+ //#then
+ expect(result).toBe("No path references here")
+ })
+
+ it("handles basePath with trailing slash", () => {
+ //#given
+ const content = "@scripts/search.py"
+ const basePath = "/skills/frontend/"
+
+ //#when
+ const result = resolveSkillPathReferences(content, basePath)
+
+ //#then
+ expect(result).toBe("/skills/frontend/scripts/search.py")
+ })
+})
diff --git a/src/shared/skill-path-resolver.ts b/src/shared/skill-path-resolver.ts
new file mode 100644
index 00000000..0ac8465e
--- /dev/null
+++ b/src/shared/skill-path-resolver.ts
@@ -0,0 +1,17 @@
+import { join } from "path"
+
+/**
+ * Resolves @path references in skill content to absolute paths.
+ *
+ * Matches @references that contain at least one slash (e.g., @scripts/search.py, @data/)
+ * to avoid false positives with decorators (@param), JSDoc tags (@ts-ignore), etc.
+ *
+ * Email addresses are excluded since they have alphanumeric characters before @.
+ */
+export function resolveSkillPathReferences(content: string, basePath: string): string {
+ const normalizedBase = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath
+ return content.replace(
+ /(? join(normalizedBase, relativePath)
+ )
+}