Headless Grok Build + Codex on Daytona does not work from the public APIs and README examples. An app has to discover and paper over nine edges.
Found while wiring OpenTag (promo video = Grok Build grok-4.5 in Daytona; docs-PR / Linear fix / Linear triage = Codex in Daytona). Packages at the time: @tanstack/ai-sandbox 0.3.0, @tanstack/ai-sandbox-daytona 0.2.1, @tanstack/ai-grok-build 0.3.1, @tanstack/ai-codex.
Goal: a default defineSandbox + gitSkill + grokBuildText / codexText + withSandbox run works on Daytona without empty commands.deny, a provider create wrapper, hardcoded /home/daytona/workspace, or hand-written ln -s for nested skills.
Items 1–5 are the ones that made Grok produce no files until we found them. Items 6–9 are the ones every app still copies as the same 20 lines.
1. Any commands.deny kills headless Grok and Codex
Silent failure. Highest priority.
mapPolicyToGrokBuildFlags sets conservative: true when commands.deny or commands.ask is non-empty. The NDJSON path then drops --always-approve and uses --permission-mode default. Headless grok -p auto-cancels every tool (User cancelled the execution) and exits with no files.
mapPolicyToCodexFlags maps the same deny/ask to approval_policy: on-request. Headless codex exec then refuses tools.
The public sandbox README shows this as the example:
deny: ['sudo *','rm -rf *']
That example is enough to break both harnesses.
Where
packages/ai-grok-build/src/adapters/policy-map.ts (hasAskOrDeny → conservative)packages/ai-grok-build/src/adapters/text.ts (alwaysApprove = !readOnly && !conservative)packages/ai-codex/src/adapters/policy-map.ts (hasAskOrDeny → on-request)packages/ai-sandbox/README.md (deny example)- Tests encode the current trap:
packages/ai-grok-build/tests/policy-map.test.ts (deny: ['rm *'] → conservative)
Fix
Do not treat deny as “ask a human.” Map deny onto the harness deny list (or ignore it for the approval flag when default: 'allow'). Only flip conservative / on-request when default is deny/ask, or when commands.ask is set.
Update the README example so a deny list is legal on a headless run.
2. gitSkill clone does not create .tanstack-skills
Bootstrap clones to <root>/.tanstack-skills/<basename>. git clone does not create parent dirs. The clone fails.
Where
packages/ai-sandbox/src/bootstrap.ts (handle.git.clone({ url, dir }))packages/ai-sandbox/src/git-exec.ts (clone runs git clone … <target> with no mkdir -p)packages/ai-sandbox/src/agents-file.ts (resolveGitSkillDir)
Daytona create only mkdir -ps the workdir, not .tanstack-skills.
Fix
mkdir -p the parent in bootstrapWorkspace or in createExecBackedGit before clone. Every provider hits this, not only Daytona.
3. Skill projector links the repo basename, not each SKILL.md
gitSkill({ repo: 'AlemTuzlak/skills' }) clones the whole repo. The Grok projector then does:
.tanstack-skills/skills → .grok/skills/skills
Grok only loads a skill when .grok/skills/<name>/SKILL.md exists. Nested packs never load. Same basename-only link on Claude and Codex projectors.
Where
packages/ai-grok-build/src/adapters/projection.ts (projectGitSkills → target = skillsDir/basenameOf(source))packages/ai-claude-code/src/adapters/projection.tspackages/ai-codex/src/adapters/projection.tspackages/ai-acp/src/adapters/projection.ts
Fix
Walk the clone for SKILL.md and link each skill by its folder name. That is what npx skills add already does.
4. Virtual /workspace is not remapped inside shell strings
Daytona remaps cwd and fs paths. It does not rewrite paths inside git clone … /workspace/.tanstack-skills/foo.
Default workspace.root is /workspace (DEFAULT_WORKSPACE_ROOT). A default Daytona + gitSkill clone lands in the wrong place, or fails.
Codex already knows this trap: it refuses to pass --cd /workspace for the same reason (packages/ai-codex/src/adapters/text.ts).
Where
packages/ai-sandbox-daytona/src/handle.ts (abs() only on cwd / fs, not on git dir)packages/ai-sandbox/src/bootstrap.ts (clones against workspace.root ?? '/workspace')packages/ai-sandbox/src/git-exec.ts (passes dir straight into the shell)
Fix (pick one)
- Daytona
git.clone remaps /workspace/… the same way fs does - or bootstrap clones against
handle.workspaceRoot, not workspace.root ?? '/workspace'
Then apps can keep the virtual root and it still works.
5. Grok default protocol is ACP, and ACP does not journal
Default is 'acp'. ACP has no NDJSON journal. withSandbox durability then warns, and a host restart cannot resume. An ATTACH on the ACP path throws DurableAttachNotSupportedError.
Where
packages/ai-grok-build/src/adapters/text.ts (protocol ?? 'acp', chatStreamAcpdurable: false)packages/ai-grok-build/src/provider-options.ts
Fix
If durability is wired, use streaming-json (or journal ACP). Do not default a durable sandbox run onto a path that cannot recover.
6. --always-approve still opens Plan Mode
Grok still hits Plan Mode and the CLI update check. Those are not covered by the policy flag.
Fix
On the headless NDJSON path, when policy is permissive, add --no-plan and --no-auto-update. Apps should not need to know Grok CLI flags to run unattended.
7. Daytona advertises snapshots: false
Daytona can snapshot. The adapter sets snapshots: false, so lifecycle.snapshot: 'after-setup' is ignored (caps.snapshots ? 'after-setup' : 'none').
Every new box re-runs apt, Grok install, ffmpeg, and skill clones. A failed box is also unsafe to reuse, because setup runs only on create.
Where
packages/ai-sandbox-daytona/src/handle.ts (DAYTONA_CAPS.snapshots: false)packages/ai-sandbox/src/sandbox.ts (effective snapshot strategy)
Fix
Implement snapshot/restore on the Daytona adapter, then honor after-setup.
8. Codex default sandbox is nested bubblewrap
Default sandboxMode is workspace-write. Inside Daytona that is:
bwrap: No permissions to create a new namespace
Where
packages/ai-codex/src/adapters/text.ts (default 'workspace-write')
Fix
Detect the nested-bwrap failure and fall back, or default to danger-full-access when the provider has no user namespaces. Isolation is then the Daytona VM + defineSandboxPolicy. The ts-react-chat / Cloudflare examples already do this by hand.
9. Daytona user is not root
Bare apt-get fails with Acquire (13: Permission denied) on /var/lib/apt. The user daytona has passwordless sudo.
Fix (any of)
- Document that setup must use
sudo -n and must not deny sudo * - Ship a
privileged() / sudoSerial() helper for setup commands - Or recommend / ship a snapshot that already has git, gh, ffmpeg, and the Grok CLI
GROK_CLI_INSTALL_COMMAND already falls back to GCS when Daytona blocks x.ai. That part is correct. Keep it.
Suggested order
- Item 1 — deny-list must not flip headless Grok/Codex into auto-cancel (plus README)
- Item 2 —
mkdir -p before gitSkill clone - Item 3 — project nested
SKILL.md dirs by skill name - Item 4 — remap
/workspace in Daytona git.clone / bootstrap - Item 5 — durable Grok runs must journal
- Item 6 — headless Grok
--no-plan --no-auto-update - Item 8 — Codex: no nested bwrap on Daytona-like providers
- Item 7 — Daytona
snapshots: true + after-setup - Item 9 — sudo / snapshot / docs
Out of scope (not library bugs)
killableProcesses: false on Daytona is honest. kill() only stops the client poll. Cancel = destroy.daytona-medium missing Grok CLI / ffmpeg is snapshot content, not an adapter bug.
Acceptance
A new app can do all of the following on Daytona and get a working headless run:
daytonaSandbox({ apiKey, snapshot: 'daytona-medium' }) with no workdirdefineWorkspace({ skills: [gitSkill({ repo: 'owner/skills-pack' })] }) with default root (/workspace)defineSandboxPolicy({ default: 'allow', commands: { deny: ['rm -rf /'] } })grokBuildText(model) with durability wired (no protocol / extraArgs required)codexText(model) without sandboxMode: 'danger-full-access'
No provider create wrapper. No hand ln -s of nested skills. No empty deny list as a load-bearing hack.
Headless Grok Build + Codex on Daytona does not work from the public APIs and README examples. An app has to discover and paper over nine edges.
Found while wiring OpenTag (promo video = Grok Build
grok-4.5in Daytona; docs-PR / Linear fix / Linear triage = Codex in Daytona). Packages at the time:@tanstack/ai-sandbox0.3.0,@tanstack/ai-sandbox-daytona0.2.1,@tanstack/ai-grok-build0.3.1,@tanstack/ai-codex.Goal: a default
defineSandbox+gitSkill+grokBuildText/codexText+withSandboxrun works on Daytona without emptycommands.deny, a providercreatewrapper, hardcoded/home/daytona/workspace, or hand-writtenln -sfor nested skills.Items 1–5 are the ones that made Grok produce no files until we found them. Items 6–9 are the ones every app still copies as the same 20 lines.
1. Any
commands.denykills headless Grok and CodexSilent failure. Highest priority.
mapPolicyToGrokBuildFlagssetsconservative: truewhencommands.denyorcommands.askis non-empty. The NDJSON path then drops--always-approveand uses--permission-mode default. Headlessgrok -pauto-cancels every tool (User cancelled the execution) and exits with no files.mapPolicyToCodexFlagsmaps the same deny/ask toapproval_policy: on-request. Headlesscodex execthen refuses tools.The public sandbox README shows this as the example:
That example is enough to break both harnesses.
Where
packages/ai-grok-build/src/adapters/policy-map.ts(hasAskOrDeny→conservative)packages/ai-grok-build/src/adapters/text.ts(alwaysApprove = !readOnly && !conservative)packages/ai-codex/src/adapters/policy-map.ts(hasAskOrDeny→on-request)packages/ai-sandbox/README.md(deny example)packages/ai-grok-build/tests/policy-map.test.ts(deny: ['rm *']→ conservative)Fix
Do not treat deny as “ask a human.” Map deny onto the harness deny list (or ignore it for the approval flag when
default: 'allow'). Only flip conservative /on-requestwhendefaultisdeny/ask, or whencommands.askis set.Update the README example so a deny list is legal on a headless run.
2.
gitSkillclone does not create.tanstack-skillsBootstrap clones to
<root>/.tanstack-skills/<basename>.git clonedoes not create parent dirs. The clone fails.Where
packages/ai-sandbox/src/bootstrap.ts(handle.git.clone({ url, dir }))packages/ai-sandbox/src/git-exec.ts(clonerunsgit clone … <target>with nomkdir -p)packages/ai-sandbox/src/agents-file.ts(resolveGitSkillDir)Daytona
createonlymkdir -ps the workdir, not.tanstack-skills.Fix
mkdir -pthe parent inbootstrapWorkspaceor increateExecBackedGitbefore clone. Every provider hits this, not only Daytona.3. Skill projector links the repo basename, not each
SKILL.mdgitSkill({ repo: 'AlemTuzlak/skills' })clones the whole repo. The Grok projector then does:Grok only loads a skill when
.grok/skills/<name>/SKILL.mdexists. Nested packs never load. Same basename-only link on Claude and Codex projectors.Where
packages/ai-grok-build/src/adapters/projection.ts(projectGitSkills→target = skillsDir/basenameOf(source))packages/ai-claude-code/src/adapters/projection.tspackages/ai-codex/src/adapters/projection.tspackages/ai-acp/src/adapters/projection.tsFix
Walk the clone for
SKILL.mdand link each skill by its folder name. That is whatnpx skills addalready does.4. Virtual
/workspaceis not remapped inside shell stringsDaytona remaps
cwdandfspaths. It does not rewrite paths insidegit clone … /workspace/.tanstack-skills/foo.Default
workspace.rootis/workspace(DEFAULT_WORKSPACE_ROOT). A default Daytona +gitSkillclone lands in the wrong place, or fails.Codex already knows this trap: it refuses to pass
--cd /workspacefor the same reason (packages/ai-codex/src/adapters/text.ts).Where
packages/ai-sandbox-daytona/src/handle.ts(abs()only oncwd/fs, not on gitdir)packages/ai-sandbox/src/bootstrap.ts(clones againstworkspace.root ?? '/workspace')packages/ai-sandbox/src/git-exec.ts(passesdirstraight into the shell)Fix (pick one)
git.cloneremaps/workspace/…the same wayfsdoeshandle.workspaceRoot, notworkspace.root ?? '/workspace'Then apps can keep the virtual root and it still works.
5. Grok default protocol is ACP, and ACP does not journal
Default is
'acp'. ACP has no NDJSON journal.withSandboxdurability then warns, and a host restart cannot resume. An ATTACH on the ACP path throwsDurableAttachNotSupportedError.Where
packages/ai-grok-build/src/adapters/text.ts(protocol ?? 'acp',chatStreamAcpdurable: false)packages/ai-grok-build/src/provider-options.tsFix
If durability is wired, use
streaming-json(or journal ACP). Do not default a durable sandbox run onto a path that cannot recover.6.
--always-approvestill opens Plan ModeGrok still hits Plan Mode and the CLI update check. Those are not covered by the policy flag.
Fix
On the headless NDJSON path, when policy is permissive, add
--no-planand--no-auto-update. Apps should not need to know Grok CLI flags to run unattended.7. Daytona advertises
snapshots: falseDaytona can snapshot. The adapter sets
snapshots: false, solifecycle.snapshot: 'after-setup'is ignored (caps.snapshots ? 'after-setup' : 'none').Every new box re-runs apt, Grok install, ffmpeg, and skill clones. A failed box is also unsafe to reuse, because setup runs only on create.
Where
packages/ai-sandbox-daytona/src/handle.ts(DAYTONA_CAPS.snapshots: false)packages/ai-sandbox/src/sandbox.ts(effective snapshot strategy)Fix
Implement snapshot/restore on the Daytona adapter, then honor
after-setup.8. Codex default sandbox is nested bubblewrap
Default
sandboxModeisworkspace-write. Inside Daytona that is:Where
packages/ai-codex/src/adapters/text.ts(default'workspace-write')Fix
Detect the nested-bwrap failure and fall back, or default to
danger-full-accesswhen the provider has no user namespaces. Isolation is then the Daytona VM +defineSandboxPolicy. The ts-react-chat / Cloudflare examples already do this by hand.9. Daytona user is not root
Bare
apt-getfails withAcquire (13: Permission denied)on/var/lib/apt. The userdaytonahas passwordlesssudo.Fix (any of)
sudo -nand must not denysudo *privileged()/sudoSerial()helper for setup commandsGROK_CLI_INSTALL_COMMANDalready falls back to GCS when Daytona blocksx.ai. That part is correct. Keep it.Suggested order
mkdir -pbeforegitSkillcloneSKILL.mddirs by skill name/workspacein Daytonagit.clone/ bootstrap--no-plan --no-auto-updatesnapshots: true+after-setupOut of scope (not library bugs)
killableProcesses: falseon Daytona is honest.kill()only stops the client poll. Cancel = destroy.daytona-mediummissing Grok CLI / ffmpeg is snapshot content, not an adapter bug.Acceptance
A new app can do all of the following on Daytona and get a working headless run:
daytonaSandbox({ apiKey, snapshot: 'daytona-medium' })with noworkdirdefineWorkspace({ skills: [gitSkill({ repo: 'owner/skills-pack' })] })with defaultroot(/workspace)defineSandboxPolicy({ default: 'allow', commands: { deny: ['rm -rf /'] } })grokBuildText(model)with durability wired (noprotocol/extraArgsrequired)codexText(model)withoutsandboxMode: 'danger-full-access'No provider
createwrapper. No handln -sof nested skills. No empty deny list as a load-bearing hack.