feat(onboard): add skip bootstrap flag (#71218)

This commit is contained in:
Patrick Erichsen
2026-04-24 12:42:00 -07:00
committed by GitHub
parent 0f689d22f4
commit 8226a3f8fe
16 changed files with 140 additions and 10 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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,
},
},
}
```

View File

@@ -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.

View File

@@ -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)

View File

@@ -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(

View File

@@ -133,6 +133,7 @@ export function registerOnboardCommand(program: Command) {
.option("--daemon-runtime <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),

View File

@@ -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<string, unknown>,
["agents", "defaults", "skipBootstrap"],
true,
);
return next;
}

View File

@@ -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);

View File

@@ -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

View File

@@ -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,

View File

@@ -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;

View File

@@ -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();

View File

@@ -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;