#!/usr/bin/env python3 """Mark Claude Code's onboarding as complete in ~/.claude.json. Claude Code re-runs its first-run wizard whenever `lastOnboardingVersion` does not match the installed version. In a terminal that is a one-key prompt; for an unattended agent it is a silent, permanent hang on the theme picker — no error, no log line, and the container looks like it started fine. It also pre-accepts the workspace's trust prompt. That is a SECOND, separate first-run gate: even past onboarding, Claude Code asks "is this a project you trust?" per directory, and this repo's settings pre-approve 442 tool permissions so the prompt is emphatic about it. Unattended, it is another silent hang. Pre-accepting is safe here precisely because the trust question is being answered by the person who built and launched the container, for their own repository — it is not a judgement being made on their behalf about someone else's code. Usage: seed-claude-config.py [workspace...] """ import json import os import sys def main() -> int: if len(sys.argv) < 3: print(f"usage: {sys.argv[0]} [workspace...]", file=sys.stderr) return 2 path, version = sys.argv[1], sys.argv[2] workspaces = sys.argv[3:] cfg = {} if os.path.exists(path) and os.path.getsize(path): try: with open(path) as f: cfg = json.load(f) except (OSError, ValueError): # A corrupt or partial config is not worth failing the container # over — start from empty rather than block the run. cfg = {} if not isinstance(cfg, dict): cfg = {} cfg["hasCompletedOnboarding"] = True cfg["lastOnboardingVersion"] = version cfg.setdefault("theme", "dark") # The THIRD interactive gate: --dangerously-skip-permissions shows a # "Bypass Permissions mode / you accept all responsibility" confirmation on # first use. Key name taken from the shipped binary's own strings, not # guessed. Accepting it here is the whole point of `sylph-agent loose` — # the container is the sandbox that warning asks you to provide. cfg["bypassPermissionsModeAccepted"] = True # A FOURTH gate, and this one fires mid-session rather than at startup, so # the pty wrapper has already handed over by then: an upsell asking whether # to try the fullscreen renderer. It is shown while # `fullscreenUpsellSeenCount` is below an internal threshold, so park it far # above. Found by reading the shipped binary's strings, same as the others. cfg["fullscreenUpsellSeenCount"] = 9999 # An auto-update mid-run would restart the process and lose the loop's # scheduled wake-up, so pin the version the container was built with. cfg["autoUpdates"] = False projects = cfg.setdefault("projects", {}) if not isinstance(projects, dict): projects = cfg["projects"] = {} for ws in workspaces: entry = projects.setdefault(ws, {}) if not isinstance(entry, dict): entry = projects[ws] = {} entry["hasTrustDialogAccepted"] = True entry.setdefault("projectOnboardingSeenCount", 1) entry["hasClaudeMdExternalIncludesApproved"] = True entry["hasClaudeMdExternalIncludesWarningShown"] = True tmp = path + ".tmp" with open(tmp, "w") as f: json.dump(cfg, f, indent=2) os.replace(tmp, path) return 0 if __name__ == "__main__": sys.exit(main())