diff --git a/docs/tools/skills.md b/docs/tools/skills.md
index 10913863f577..47ea262e9da8 100644
--- a/docs/tools/skills.md
+++ b/docs/tools/skills.md
@@ -55,8 +55,11 @@ snapshot refresh and sandbox synchronization. Sandboxed runs read the
materialized copies, not the original host paths.
Managed worktree sessions keep their recorded canonical workspace as the skill
-source. A selected nested workspace stays nested: discovery does not walk up to
-its parent repository. Installing OpenClaw from a repository does not make that
+source. The configured agent workspace remains the primary skill source even when
+the session executes in a worktree; only selecting that worktree as the agent's
+workspace gives its skills primary precedence. A selected nested workspace stays
+nested: discovery does not walk up to its parent repository. Installing OpenClaw
+from a repository does not make that
repository's `.agents/skills/` a global bundled skill source.
Each discovery pass reports one summary per winning/losing discovery root and
diff --git a/src/agents/agent-command.ts b/src/agents/agent-command.ts
index fbb9045758b8..1207473110c3 100644
--- a/src/agents/agent-command.ts
+++ b/src/agents/agent-command.ts
@@ -445,7 +445,6 @@ async function agentCommandInternal(
sessionAgentId,
lifecycleGeneration,
runId,
- workspaceDir,
executionWorkspaceDir:
sessionEntry?.worktree?.canonicalWorkspaceDir ?? cwd ?? workspaceDir,
watchSkills,
diff --git a/src/agents/cli-runner.test-helpers.ts b/src/agents/cli-runner.test-helpers.ts
index 7a320624d1f3..e6b81f66c367 100644
--- a/src/agents/cli-runner.test-helpers.ts
+++ b/src/agents/cli-runner.test-helpers.ts
@@ -44,6 +44,27 @@ export type TestCliBackendParams = {
systemPromptWhen?: "first" | "always" | "never";
};
+export function createCliRepositorySkillFixture(dir: string, taskDir: string, managed: boolean) {
+ const canonicalDir = path.join(dir, "canonical", "packages", "app");
+ const skillDir = path.join(managed ? canonicalDir : taskDir, ".agents", "skills", "task-proof");
+ fs.mkdirSync(skillDir, { recursive: true });
+ fs.writeFileSync(
+ path.join(skillDir, "SKILL.md"),
+ "---\nname: task-proof\ndescription: Task-local proof\n---\n# Proof instructions\n",
+ );
+ if (managed) {
+ for (const source of [".agents/skills", "skills"]) {
+ const worktreeSkillDir = path.join(taskDir, source, "task-proof");
+ fs.mkdirSync(worktreeSkillDir, { recursive: true });
+ fs.writeFileSync(
+ path.join(worktreeSkillDir, "SKILL.md"),
+ "---\nname: task-proof\ndescription: Worktree copy\n---\n# Changed instructions\n",
+ );
+ }
+ }
+ return { canonicalDir, skillDir };
+}
+
export function wrappedPluginSystemContext(text: string) {
return `---\n\nOpenClaw plugin-injected system context. This block is not workspace file content.\n\n${text}\n\n---`;
}
diff --git a/src/agents/cli-runner/prepare.test.ts b/src/agents/cli-runner/prepare.test.ts
index 56715d465bb0..bfb77efa5ca6 100644
--- a/src/agents/cli-runner/prepare.test.ts
+++ b/src/agents/cli-runner/prepare.test.ts
@@ -102,6 +102,7 @@ import { testing as cliBackendsTesting } from "../cli-backends.test-support.js";
import {
buildDefaultTestCliBackend,
createCliRunnerPrepareFixture,
+ createCliRepositorySkillFixture,
createTestMcpLoopbackClientGrant,
createTestMcpLoopbackServer,
createTestMcpLoopbackServerConfig,
@@ -3152,15 +3153,11 @@ describe("prepareCliRunContext", () => {
it.each([false, true])("uses admitted CLI repository skills (managed=%s)", async (managed) => {
const { dir } = fixture.session;
const taskDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-cli-task-"));
- const canonicalDir = path.join(dir, "canonical", "packages", "app");
- const skillDir = path.join(managed ? canonicalDir : taskDir, ".agents", "skills", "task-proof");
- fs.mkdirSync(skillDir, { recursive: true });
- fs.writeFileSync(
- path.join(skillDir, "SKILL.md"),
- "---\nname: task-proof\ndescription: Task-local proof\n---\n# Proof instructions\n",
- );
+ const { canonicalDir, skillDir } = createCliRepositorySkillFixture(dir, taskDir, managed);
try {
const context = await fixture.prepare({
+ config: { agents: { defaults: { workspace: dir } } },
+ workspaceDir: taskDir,
cwd: taskDir,
skillsSnapshot: undefined,
...(managed
@@ -3184,6 +3181,7 @@ describe("prepareCliRunContext", () => {
expect(context.systemPrompt).not.toContain(`Working directory: ${dir}`);
expect(context.systemPrompt).toContain("task-proof");
expect(context.systemPrompt).toContain(path.join(skillDir, "SKILL.md"));
+ expect(context.systemPrompt).not.toContain("Worktree copy");
} finally {
fs.rmSync(taskDir, { recursive: true, force: true });
}
diff --git a/src/agents/cli-runner/prepare.ts b/src/agents/cli-runner/prepare.ts
index 69a737787dda..ad8f2a0b5785 100644
--- a/src/agents/cli-runner/prepare.ts
+++ b/src/agents/cli-runner/prepare.ts
@@ -50,10 +50,6 @@ import {
} from "../../routing/session-key.js";
import { annotateInterSessionPromptText } from "../../sessions/input-provenance.js";
import { captureAsyncWorkTracker } from "../../shared/async-work-scope.js";
-import { resolveSkillsPrompt } from "../../skills/loading/workspace-skill-prompt.js";
-import { resolveEmbeddedRunSkillEntries } from "../../skills/runtime/embedded-run-entries.js";
-import { resolveReusableWorkspaceSkillSnapshot } from "../../skills/runtime/session-snapshot.js";
-import type { SkillUsagePath } from "../../skills/types.js";
import { resolveUserPath } from "../../utils.js";
import { normalizeMessageChannel } from "../../utils/message-channel.js";
import { resolveAdmittedRunActiveAssertion } from "../admitted-run-context.js";
@@ -107,11 +103,7 @@ import {
mergeForcedEmbeddedAttemptToolsAllow,
} from "../embedded-agent-runner/run/attempt-tool-construction-plan.js";
import { buildCurrentInboundPrompt } from "../embedded-agent-runner/run/runtime-context-prompt.js";
-import {
- mapSandboxSkillEntriesForPrompt,
- remapSkillReferencePaths,
- resolveSandboxSkillRuntimeInputs,
-} from "../embedded-agent-runner/sandbox-skills.js";
+import { remapSkillReferencePaths } from "../embedded-agent-runner/sandbox-skills.js";
import { selectContextEngineForTranscriptHost } from "../harness/context-engine-logical-turn.js";
import { drainPendingContextEngineTurnsBeforeRun } from "../harness/context-engine-turn-attempt.js";
import { createAgentQuestionAnswerAuthority } from "../harness/host-private-capabilities.js";
@@ -125,7 +117,6 @@ import {
type PreparedRootedExecutionCapability,
} from "../rooted-run-params.js";
import { collectRuntimeChannelCapabilities } from "../runtime-capabilities.js";
-import { ensureSandboxWorkspaceForSession } from "../sandbox.js";
import { resolveSandboxRuntimeStatus } from "../sandbox/runtime-status.js";
import { buildSystemPromptReport } from "../system-prompt-report.js";
import { appendModelIdentitySystemPrompt, buildModelIdentityPromptLine } from "../system-prompt.js";
@@ -179,6 +170,7 @@ import {
loadCliSessionPromptContext,
resolveAutoCliSessionReseedHistoryChars,
} from "./session-history.js";
+import { resolveCliSkillsPrompt } from "./skills-prompt.js";
import { prepareCliReplyToolAuthority } from "./tool-authority.js";
import {
captureCliRunStartTime,
@@ -257,81 +249,6 @@ function prependCliSessionDriftUserContext(
};
}
-async function resolveCliSkillsPrompt(params: {
- assertCurrent: () => void;
- agentId: string;
- config: RunCliAgentParams["config"];
- sessionKey: string;
- skillsSnapshot: RunCliAgentParams["skillsSnapshot"];
- workspaceDir: string;
- executionWorkspaceDir: string;
-}): Promise<{ prompt: string; usagePaths?: SkillUsagePath[] }> {
- params.assertCurrent();
- const skillsSnapshot =
- params.skillsSnapshot ??
- (
- await resolveReusableWorkspaceSkillSnapshot({
- assertCurrent: params.assertCurrent,
- workspaceDir: params.workspaceDir,
- executionWorkspaceDir: params.executionWorkspaceDir,
- config: params.config ?? {},
- agentId: params.agentId,
- watch: false,
- })
- ).snapshot;
- params.assertCurrent();
- const sandboxWorkspace = await ensureSandboxWorkspaceForSession({
- skillsSnapshot,
- config: params.config,
- agentId: params.agentId,
- sessionKey: params.sessionKey,
- workspaceDir: params.workspaceDir,
- });
- params.assertCurrent();
- const {
- skillsEligibility,
- skillUsagePaths,
- skillsPromptWorkspaceDir,
- skillsSnapshot: skillsSnapshotForRun,
- skillsWorkspaceDir,
- workspaceOnly,
- } = resolveSandboxSkillRuntimeInputs({
- sandbox: sandboxWorkspace ? { ...sandboxWorkspace, enabled: true } : undefined,
- skillsAnchorWorkspace: sandboxWorkspace?.workspaceDir ?? params.workspaceDir,
- skillsSnapshot,
- });
- const { shouldLoadSkillEntries, skillEntries, loadSkillEntries, preserveEntryOrder } =
- await resolveEmbeddedRunSkillEntries({
- assertCurrent: params.assertCurrent,
- workspaceDir: skillsWorkspaceDir,
- ...(sandboxWorkspace ? {} : { executionWorkspaceDir: params.executionWorkspaceDir }),
- config: params.config,
- agentId: params.agentId,
- eligibility: skillsEligibility,
- skillsSnapshot: skillsSnapshotForRun,
- workspaceOnly,
- });
- const promptSkillEntries = mapSandboxSkillEntriesForPrompt({
- entries: shouldLoadSkillEntries ? skillEntries : undefined,
- skillsWorkspaceDir,
- skillsPromptWorkspaceDir,
- });
- return {
- ...(sandboxWorkspace ? { usagePaths: skillUsagePaths } : {}),
- prompt: await resolveSkillsPrompt({
- assertCurrent: params.assertCurrent,
- skillsSnapshot: skillsSnapshotForRun,
- entries: promptSkillEntries,
- ...(sandboxWorkspace ? {} : { loadEntries: loadSkillEntries }),
- workspaceDir: skillsPromptWorkspaceDir,
- config: params.config,
- agentId: params.agentId,
- eligibility: skillsEligibility,
- preserveEntryOrder,
- }),
- };
-}
-
/** Overrides preparation dependencies for CLI runner tests. */
function setCliRunnerPrepareTestDeps(overrides: Partial): void {
Object.assign(prepareDeps, overrides);
diff --git a/src/agents/cli-runner/skills-prompt.ts b/src/agents/cli-runner/skills-prompt.ts
new file mode 100644
index 000000000000..88fbbb1e8383
--- /dev/null
+++ b/src/agents/cli-runner/skills-prompt.ts
@@ -0,0 +1,87 @@
+import { resolveSkillsPrompt } from "../../skills/loading/workspace-skill-prompt.js";
+import { resolveEmbeddedRunSkillEntries } from "../../skills/runtime/embedded-run-entries.js";
+import { resolveReusableWorkspaceSkillSnapshot } from "../../skills/runtime/session-snapshot.js";
+import type { SkillUsagePath } from "../../skills/types.js";
+import { resolveAgentWorkspaceDir } from "../agent-scope-config.js";
+import {
+ mapSandboxSkillEntriesForPrompt,
+ resolveSandboxSkillRuntimeInputs,
+} from "../embedded-agent-runner/sandbox-skills.js";
+import { ensureSandboxWorkspaceForSession } from "../sandbox.js";
+import type { RunCliAgentParams } from "./types.js";
+
+export async function resolveCliSkillsPrompt(params: {
+ assertCurrent: () => void;
+ agentId: string;
+ config: RunCliAgentParams["config"];
+ sessionKey: string;
+ skillsSnapshot: RunCliAgentParams["skillsSnapshot"];
+ workspaceDir: string;
+ executionWorkspaceDir: string;
+}): Promise<{ prompt: string; usagePaths?: SkillUsagePath[] }> {
+ params.assertCurrent();
+ const agentWorkspaceDir = resolveAgentWorkspaceDir(params.config ?? {}, params.agentId);
+ const skillsSnapshot =
+ params.skillsSnapshot ??
+ (
+ await resolveReusableWorkspaceSkillSnapshot({
+ assertCurrent: params.assertCurrent,
+ workspaceDir: agentWorkspaceDir,
+ executionWorkspaceDir: params.executionWorkspaceDir,
+ config: params.config ?? {},
+ agentId: params.agentId,
+ watch: false,
+ })
+ ).snapshot;
+ params.assertCurrent();
+ const sandboxWorkspace = await ensureSandboxWorkspaceForSession({
+ skillsSnapshot,
+ config: params.config,
+ agentId: params.agentId,
+ sessionKey: params.sessionKey,
+ workspaceDir: params.workspaceDir,
+ });
+ params.assertCurrent();
+ const {
+ skillsEligibility,
+ skillUsagePaths,
+ skillsPromptWorkspaceDir,
+ skillsSnapshot: skillsSnapshotForRun,
+ skillsWorkspaceDir,
+ workspaceOnly,
+ } = resolveSandboxSkillRuntimeInputs({
+ sandbox: sandboxWorkspace ? { ...sandboxWorkspace, enabled: true } : undefined,
+ skillsAnchorWorkspace: sandboxWorkspace?.workspaceDir ?? agentWorkspaceDir,
+ skillsSnapshot,
+ });
+ const { shouldLoadSkillEntries, skillEntries, loadSkillEntries, preserveEntryOrder } =
+ await resolveEmbeddedRunSkillEntries({
+ assertCurrent: params.assertCurrent,
+ workspaceDir: skillsWorkspaceDir,
+ ...(sandboxWorkspace ? {} : { executionWorkspaceDir: params.executionWorkspaceDir }),
+ config: params.config,
+ agentId: params.agentId,
+ eligibility: skillsEligibility,
+ skillsSnapshot: skillsSnapshotForRun,
+ workspaceOnly,
+ });
+ const promptSkillEntries = mapSandboxSkillEntriesForPrompt({
+ entries: shouldLoadSkillEntries ? skillEntries : undefined,
+ skillsWorkspaceDir,
+ skillsPromptWorkspaceDir,
+ });
+ return {
+ ...(sandboxWorkspace ? { usagePaths: skillUsagePaths } : {}),
+ prompt: await resolveSkillsPrompt({
+ assertCurrent: params.assertCurrent,
+ skillsSnapshot: skillsSnapshotForRun,
+ entries: promptSkillEntries,
+ ...(sandboxWorkspace ? {} : { loadEntries: loadSkillEntries }),
+ workspaceDir: skillsPromptWorkspaceDir,
+ config: params.config,
+ agentId: params.agentId,
+ eligibility: skillsEligibility,
+ preserveEntryOrder,
+ }),
+ };
+}
diff --git a/src/agents/command/session-preparation.test.ts b/src/agents/command/session-preparation.test.ts
index 3b9fb8a7e342..a35dc5522b1f 100644
--- a/src/agents/command/session-preparation.test.ts
+++ b/src/agents/command/session-preparation.test.ts
@@ -73,7 +73,6 @@ it.each([
sessionAgentId: "main",
lifecycleGeneration,
runId,
- workspaceDir: "/workspace",
executionWorkspaceDir: "/workspace",
watchSkills: false,
isNewSession: false,
diff --git a/src/agents/command/session-preparation.ts b/src/agents/command/session-preparation.ts
index 9ac9c4fb2365..9474e7591a33 100644
--- a/src/agents/command/session-preparation.ts
+++ b/src/agents/command/session-preparation.ts
@@ -18,6 +18,7 @@ import {
buildCurrentRunRestartRecoveryClaim,
prepareCommandHarnessCompletionRecovery,
} from "../agent-command-restart-recovery.js";
+import { resolveAgentWorkspaceDir } from "../agent-scope-config.js";
import { persistAgentSession } from "./attempt-execution.shared.js";
import { resolveAgentRunContext } from "./run-context.js";
import { loadExecDefaultsRuntime, loadSkillsRuntime } from "./runtime-loaders.js";
@@ -98,7 +99,6 @@ export async function prepareEmbeddedSessionState(params: {
sessionAgentId: string;
lifecycleGeneration: string;
runId: string;
- workspaceDir: string;
executionWorkspaceDir: string;
watchSkills: boolean;
isNewSession: boolean;
@@ -146,7 +146,7 @@ export async function prepareEmbeddedSessionState(params: {
agentId: params.sessionAgentId,
});
const skillSnapshotState = await resolveReusableWorkspaceSkillSnapshot({
- workspaceDir: params.workspaceDir,
+ workspaceDir: resolveAgentWorkspaceDir(params.cfg, params.sessionAgentId),
executionWorkspaceDir: params.executionWorkspaceDir,
config: params.cfg,
agentId: params.sessionAgentId,
diff --git a/src/commands/agent.test.ts b/src/commands/agent.test.ts
index ead70b4137d8..dfa64ee0bc95 100644
--- a/src/commands/agent.test.ts
+++ b/src/commands/agent.test.ts
@@ -845,16 +845,15 @@ describe("agentCommand", () => {
{
message: "inspect this repo",
sessionKey,
+ workspaceDir: worktree.path,
allowModelOverride: false,
},
runtime,
);
- expect(resolveReusableWorkspaceSkillSnapshot).toHaveBeenCalledWith(
- expect.objectContaining({
- executionWorkspaceDir: canonicalWorkspace,
- }),
- );
+ const skillRoots = vi.mocked(resolveReusableWorkspaceSkillSnapshot).mock.calls.at(-1)?.[0];
+ expect(skillRoots?.workspaceDir).toBe(path.join(home, "openclaw"));
+ expect(skillRoots?.executionWorkspaceDir).toBe(canonicalWorkspace);
});
});