diff --git a/docs/cli/onboard.md b/docs/cli/onboard.md index 814f3249388..19bf771f4c5 100644 --- a/docs/cli/onboard.md +++ b/docs/cli/onboard.md @@ -23,6 +23,7 @@ Interactive onboarding for local or remote Gateway setup. openclaw onboard openclaw onboard --flow quickstart openclaw onboard --flow manual +openclaw onboard --skip-bootstrap openclaw onboard --mode remote --remote-url wss://gateway-host:18789 ``` @@ -115,6 +116,7 @@ Non-interactive local gateway health: - Unless you pass `--skip-health`, onboarding waits for a reachable local gateway before it exits successfully. - `--install-daemon` starts the managed gateway install path first. Without it, you must already have a local gateway running, for example `openclaw gateway run`. - If you only want config/workspace/bootstrap writes in automation, use `--skip-health`. +- If you manage workspace files yourself, pass `--skip-bootstrap` to set `agents.defaults.skipBootstrap: true` and skip creating `AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, `HEARTBEAT.md`, and `BOOTSTRAP.md`. - On native Windows, `--install-daemon` tries Scheduled Tasks first and falls back to a per-user Startup-folder login item if task creation is denied. Interactive onboarding behavior with reference mode: diff --git a/docs/concepts/agent-workspace.md b/docs/concepts/agent-workspace.md index 043e9ec78bf..42a6d46735c 100644 --- a/docs/concepts/agent-workspace.md +++ b/docs/concepts/agent-workspace.md @@ -28,8 +28,10 @@ inside a sandbox workspace under `~/.openclaw/sandboxes`, not your host workspac ```json5 { - agent: { - workspace: "~/.openclaw/workspace", + agents: { + defaults: { + workspace: "~/.openclaw/workspace", + }, }, } ``` @@ -43,7 +45,7 @@ If you already manage the workspace files yourself, you can disable bootstrap file creation: ```json5 -{ agent: { skipBootstrap: true } } +{ agents: { defaults: { skipBootstrap: true } } } ``` ## Extra workspace folders diff --git a/docs/concepts/agent.md b/docs/concepts/agent.md index 264359cdfe7..3277b7d4ff5 100644 --- a/docs/concepts/agent.md +++ b/docs/concepts/agent.md @@ -44,7 +44,7 @@ If a file is missing, OpenClaw injects a single “missing file” marker line ( To disable bootstrap file creation entirely (for pre-seeded workspaces), set: ```json5 -{ agent: { skipBootstrap: true } } +{ agents: { defaults: { skipBootstrap: true } } } ``` ## Built-in tools diff --git a/docs/start/bootstrapping.md b/docs/start/bootstrapping.md index cb847f08c2a..d754e55ef9e 100644 --- a/docs/start/bootstrapping.md +++ b/docs/start/bootstrapping.md @@ -22,6 +22,10 @@ On the first agent run, OpenClaw bootstraps the workspace (default - Writes identity + preferences to `IDENTITY.md`, `USER.md`, `SOUL.md`. - Removes `BOOTSTRAP.md` when finished so it only runs once. +## Skipping bootstrapping + +To skip this for a pre-seeded workspace, run `openclaw onboard --skip-bootstrap`. + ## Where it runs Bootstrapping always runs on the **gateway host**. If the macOS app connects to diff --git a/docs/start/openclaw.md b/docs/start/openclaw.md index 71e5857ceab..a7f2932ea0b 100644 --- a/docs/start/openclaw.md +++ b/docs/start/openclaw.md @@ -87,8 +87,10 @@ Optional: choose a different workspace with `agents.defaults.workspace` (support ```json5 { - agent: { - workspace: "~/.openclaw/workspace", + agents: { + defaults: { + workspace: "~/.openclaw/workspace", + }, }, } ``` @@ -97,8 +99,10 @@ If you already ship your own workspace files from a repo, you can disable bootst ```json5 { - agent: { - skipBootstrap: true, + agents: { + defaults: { + skipBootstrap: true, + }, }, } ``` diff --git a/docs/start/wizard-cli-automation.md b/docs/start/wizard-cli-automation.md index 4906190f61c..d40db6ffa78 100644 --- a/docs/start/wizard-cli-automation.md +++ b/docs/start/wizard-cli-automation.md @@ -25,11 +25,14 @@ openclaw onboard --non-interactive \ --gateway-bind loopback \ --install-daemon \ --daemon-runtime node \ + --skip-bootstrap \ --skip-skills ``` Add `--json` for a machine-readable summary. +Use `--skip-bootstrap` when your automation pre-seeds workspace files and does not want onboarding to create the default bootstrap files. + Use `--secret-input-mode ref` to store env-backed refs in auth profiles instead of plaintext values. Interactive selection between env refs and configured provider refs (`file` or `exec`) is available in the onboarding flow. diff --git a/docs/start/wizard-cli-reference.md b/docs/start/wizard-cli-reference.md index d79400bb457..a81716c5c05 100644 --- a/docs/start/wizard-cli-reference.md +++ b/docs/start/wizard-cli-reference.md @@ -260,6 +260,7 @@ is only a legacy import source. Typical fields in `~/.openclaw/openclaw.json`: - `agents.defaults.workspace` +- `agents.defaults.skipBootstrap` when `--skip-bootstrap` is passed - `agents.defaults.model` / `models.providers` (if Minimax chosen) - `tools.profile` (local onboarding defaults to `"coding"` when unset; existing explicit values are preserved) - `gateway.*` (mode, bind, auth, tailscale) diff --git a/src/cli/program/register.onboard.test.ts b/src/cli/program/register.onboard.test.ts index 31183470334..97bb395ba25 100644 --- a/src/cli/program/register.onboard.test.ts +++ b/src/cli/program/register.onboard.test.ts @@ -133,6 +133,16 @@ describe("registerOnboardCommand", () => { ); }); + it("forwards --skip-bootstrap to setup wizard options", async () => { + await runCli(["onboard", "--skip-bootstrap"]); + expect(setupWizardCommandMock).toHaveBeenCalledWith( + expect.objectContaining({ + skipBootstrap: true, + }), + runtime, + ); + }); + it("parses --mistral-api-key and forwards mistralApiKey", async () => { await runCli(["onboard", "--mistral-api-key", "sk-mistral-test"]); expect(setupWizardCommandMock).toHaveBeenCalledWith( diff --git a/src/cli/program/register.onboard.ts b/src/cli/program/register.onboard.ts index 3909707f263..f1b7caa1e9d 100644 --- a/src/cli/program/register.onboard.ts +++ b/src/cli/program/register.onboard.ts @@ -133,6 +133,7 @@ export function registerOnboardCommand(program: Command) { .option("--daemon-runtime ", "Daemon runtime: node|bun") .option("--skip-channels", "Skip channel setup") .option("--skip-skills", "Skip skills setup") + .option("--skip-bootstrap", "Skip creating default agent workspace files") .option("--skip-search", "Skip search provider setup") .option("--skip-health", "Skip health check") .option("--skip-ui", "Skip Control UI/TUI prompts") @@ -189,6 +190,7 @@ export function registerOnboardCommand(program: Command) { daemonRuntime: opts.daemonRuntime as GatewayDaemonRuntime | undefined, skipChannels: Boolean(opts.skipChannels), skipSkills: Boolean(opts.skipSkills), + skipBootstrap: Boolean(opts.skipBootstrap), skipSearch: Boolean(opts.skipSearch), skipHealth: Boolean(opts.skipHealth), skipUi: Boolean(opts.skipUi), diff --git a/src/commands/onboard-config.ts b/src/commands/onboard-config.ts index 4512e483ede..ebf4ad2414d 100644 --- a/src/commands/onboard-config.ts +++ b/src/commands/onboard-config.ts @@ -1,3 +1,4 @@ +import { setConfigValueAtPath } from "../config/config-paths.js"; import type { DmScope } from "../config/types.base.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { ToolProfileId } from "../config/types.tools.js"; @@ -32,3 +33,13 @@ export function applyLocalSetupWorkspaceConfig( }, }; } + +export function applySkipBootstrapConfig(cfg: OpenClawConfig): OpenClawConfig { + const next = structuredClone(cfg); + setConfigValueAtPath( + next as Record, + ["agents", "defaults", "skipBootstrap"], + true, + ); + return next; +} diff --git a/src/commands/onboard-non-interactive.gateway.test.ts b/src/commands/onboard-non-interactive.gateway.test.ts index 13452b84ae7..08707595096 100644 --- a/src/commands/onboard-non-interactive.gateway.test.ts +++ b/src/commands/onboard-non-interactive.gateway.test.ts @@ -353,6 +353,38 @@ describe("onboard (non-interactive): gateway and remote auth", () => { }); }, 60_000); + it("persists skipBootstrap and skips workspace bootstrap creation", async () => { + ensureWorkspaceAndSessionsMock.mockClear(); + await withStateDir("state-skip-bootstrap-", async (stateDir) => { + const workspace = path.join(stateDir, "openclaw"); + + await runNonInteractiveSetup( + { + nonInteractive: true, + mode: "local", + workspace, + authChoice: "skip", + skipBootstrap: true, + skipSkills: true, + skipHealth: true, + installDaemon: false, + gatewayBind: "loopback", + }, + runtime, + ); + + const cfg = readTestConfig(); + + expect(cfg.agents?.defaults?.workspace).toBe(workspace); + expect(cfg.agents?.defaults?.skipBootstrap).toBe(true); + expect(ensureWorkspaceAndSessionsMock).toHaveBeenCalledWith( + workspace, + runtime, + expect.objectContaining({ skipBootstrap: true }), + ); + }); + }, 60_000); + it("writes gateway.remote url/token", async () => { await withStateDir("state-remote-", async (_stateDir) => { const port = getPseudoPort(30_000); diff --git a/src/commands/onboard-non-interactive/local.ts b/src/commands/onboard-non-interactive/local.ts index fca9ed7ad97..1120e3bef29 100644 --- a/src/commands/onboard-non-interactive/local.ts +++ b/src/commands/onboard-non-interactive/local.ts @@ -5,7 +5,7 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { resolveGatewayAuthToken } from "../../gateway/auth-token-resolution.js"; import type { RuntimeEnv } from "../../runtime.js"; import { DEFAULT_GATEWAY_DAEMON_RUNTIME } from "../daemon-runtime.js"; -import { applyLocalSetupWorkspaceConfig } from "../onboard-config.js"; +import { applyLocalSetupWorkspaceConfig, applySkipBootstrapConfig } from "../onboard-config.js"; import { applyWizardMetadata, DEFAULT_WORKSPACE, @@ -136,6 +136,9 @@ export async function runNonInteractiveLocalSetup(params: { }); let nextConfig: OpenClawConfig = applyLocalSetupWorkspaceConfig(baseConfig, workspaceDir); + if (opts.skipBootstrap) { + nextConfig = applySkipBootstrapConfig(nextConfig); + } const inferredAuthChoice = opts.authChoice ? undefined diff --git a/src/commands/onboard-non-interactive/remote.ts b/src/commands/onboard-non-interactive/remote.ts index 5c15e38d8f0..4a20f5cc3cd 100644 --- a/src/commands/onboard-non-interactive/remote.ts +++ b/src/commands/onboard-non-interactive/remote.ts @@ -4,6 +4,7 @@ import { logConfigUpdated } from "../../config/logging.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { type RuntimeEnv, writeRuntimeJson } from "../../runtime.js"; import { normalizeOptionalString } from "../../shared/string-coerce.js"; +import { applySkipBootstrapConfig } from "../onboard-config.js"; import { applyWizardMetadata } from "../onboard-helpers.js"; import type { OnboardOptions } from "../onboard-types.js"; @@ -34,6 +35,9 @@ export async function runNonInteractiveRemoteSetup(params: { }, }, }; + if (opts.skipBootstrap) { + nextConfig = applySkipBootstrapConfig(nextConfig); + } nextConfig = applyWizardMetadata(nextConfig, { command: "onboard", mode }); await replaceConfigFile({ nextConfig, diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index 68b57fa78be..91672cea0d8 100644 --- a/src/commands/onboard-types.ts +++ b/src/commands/onboard-types.ts @@ -76,6 +76,7 @@ export type OnboardOptions = OnboardDynamicProviderOptions & { /** @deprecated Legacy alias for `skipChannels`. */ skipProviders?: boolean; skipSkills?: boolean; + skipBootstrap?: boolean; skipSearch?: boolean; skipHealth?: boolean; skipUi?: boolean; diff --git a/src/wizard/setup.test.ts b/src/wizard/setup.test.ts index 87f42d5d7f5..8d2526ded16 100644 --- a/src/wizard/setup.test.ts +++ b/src/wizard/setup.test.ts @@ -431,6 +431,49 @@ describe("runSetupWizard", () => { expect(runTui).not.toHaveBeenCalled(); }); + it("persists skipBootstrap and skips workspace bootstrap creation when requested", async () => { + ensureWorkspaceAndSessions.mockClear(); + writeConfigFile.mockClear(); + + const workspaceDir = await makeCaseDir("skip-bootstrap-"); + const prompter = buildWizardPrompter({}); + const runtime = createRuntime(); + + await runSetupWizard( + { + acceptRisk: true, + flow: "quickstart", + authChoice: "skip", + installDaemon: false, + skipBootstrap: true, + skipChannels: true, + skipSkills: true, + skipSearch: true, + skipHealth: true, + skipUi: true, + workspace: workspaceDir, + }, + runtime, + prompter, + ); + + expect(writeConfigFile).toHaveBeenCalledWith( + expect.objectContaining({ + agents: expect.objectContaining({ + defaults: expect.objectContaining({ + skipBootstrap: true, + workspace: workspaceDir, + }), + }), + }), + ); + expect(ensureWorkspaceAndSessions).toHaveBeenCalledWith( + workspaceDir, + runtime, + expect.objectContaining({ skipBootstrap: true }), + ); + }); + it("fails fast if the auth choice prompt returns nothing", async () => { promptAuthChoiceGrouped.mockImplementationOnce(async () => undefined as never); const prompter = buildWizardPrompter(); diff --git a/src/wizard/setup.ts b/src/wizard/setup.ts index af77ce4fd9c..5608091feed 100644 --- a/src/wizard/setup.ts +++ b/src/wizard/setup.ts @@ -462,10 +462,14 @@ export async function runSetupWizard( if (mode === "remote") { const { promptRemoteGatewayConfig } = await import("../commands/onboard-remote.js"); + const { applySkipBootstrapConfig } = await import("../commands/onboard-config.js"); const { logConfigUpdated } = await loadConfigLoggingModule(); let nextConfig = await promptRemoteGatewayConfig(baseConfig, prompter, { secretInputMode: opts.secretInputMode, }); + if (opts.skipBootstrap) { + nextConfig = applySkipBootstrapConfig(nextConfig); + } nextConfig = onboardHelpers.applyWizardMetadata(nextConfig, { command: "onboard", mode }); await writeConfigFile(nextConfig); logConfigUpdated(runtime); @@ -484,8 +488,12 @@ export async function runSetupWizard( const workspaceDir = resolveUserPath(workspaceInput.trim() || onboardHelpers.DEFAULT_WORKSPACE); - const { applyLocalSetupWorkspaceConfig } = await import("../commands/onboard-config.js"); + const { applyLocalSetupWorkspaceConfig, applySkipBootstrapConfig } = + await import("../commands/onboard-config.js"); let nextConfig: OpenClawConfig = applyLocalSetupWorkspaceConfig(baseConfig, workspaceDir); + if (opts.skipBootstrap) { + nextConfig = applySkipBootstrapConfig(nextConfig); + } const authChoiceFromPrompt = opts.authChoice === undefined; let authChoice: AuthChoice | undefined = opts.authChoice;