`./sylph-agent loose [task]` starts Claude Code detached with
--dangerously-skip-permissions, running /loop on loop-task.md: work the RE
backlog one item at a time, commit to auto/* branches, never push, record
withdrawn results rather than deleting them. `logs`/`attach`/`stop` to watch and
end it. Runs -d WITHOUT --rm so the transcript survives the container exiting —
for an unattended run that is the only record of what happened.
Two things had to be fixed for an agent to survive being left alone.
MEMORY CONTINUITY. The project is now bind-mounted twice: at /work, and at its
own host path. Claude Code derives its per-project state key from the working
directory, so running at /work handed the agent an empty project instead of the
accumulated one. Verified: a loose run now reports MEMORY=yes and reads back the
same branch and backlog as the host.
FOUR INTERACTIVE GATES, each a silent permanent hang with nobody at the keyboard
-- no error, no log line, just a container that looks healthy and does nothing:
theme picker hasCompletedOnboarding + lastOnboardingVersion. Re-fires
whenever the container's Claude Code is a different version
to the host's, which is the normal case.
folder trust projects.<path>.hasTrustDialogAccepted
bypass disclaimer answered in a pty by bin/claude-autonomous. It has no config
key by design -- it wants a person to accept once, and the
person did so by launching this.
fullscreen upsell fullscreenUpsellSeenCount. This one fires MID-SESSION, after
the pty wrapper has already handed over, so it cannot be
answered the same way.
Config key names were read out of the shipped binary's own strings, not guessed.
The pty wrapper matches SINGLE WORDS. Claude Code draws its UI with
absolute-column escapes between words, so the prompt arrives as
`Yes,\x1b[13GI\x1b[15Gaccept` and a multi-word pattern never matches -- failing
in a way indistinguishable from the wrapper not running at all. It stops
matching once the session is live so nothing later is answered by accident.
~/.claude.json is now mounted read-only at a staging path and copied in, so the
container cannot rewrite the host config. Credentials stay shared read-write in
~/.claude, which is what token refresh and memory continuity need.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
67 lines
2.3 KiB
Plaintext
Executable File
67 lines
2.3 KiB
Plaintext
Executable File
#!/usr/bin/expect -f
|
|
# Start Claude Code for an unattended run, answering the first-run gates.
|
|
#
|
|
# Claude Code has three one-time interactive prompts, and every one of them is a
|
|
# silent, permanent hang for an agent with nobody at the keyboard — no error, no
|
|
# log line, just a container that looks healthy and does nothing:
|
|
#
|
|
# 1. the theme picker (first run, or whenever the installed version is
|
|
# newer than lastOnboardingVersion)
|
|
# 2. "do you trust this folder?" (per workspace)
|
|
# 3. the Bypass Permissions disclaimer (for --dangerously-skip-permissions)
|
|
#
|
|
# `seed-claude-config.py` pre-sets the config keys for 1 and 2. The disclaimer
|
|
# has no such key — it is meant to be accepted by a person once — so it is
|
|
# answered here instead. That is the honest reading of `sylph-agent loose`: the
|
|
# operator accepted it by choosing to run this, and the container is exactly the
|
|
# sandbox the warning asks for.
|
|
#
|
|
# ── Why the patterns are single words ──
|
|
# Claude Code draws its UI with ABSOLUTE COLUMN escapes between words, so the
|
|
# prompt arrives on the wire as
|
|
#
|
|
# 2.\x1b[8GYes,\x1b[13GI\x1b[15Gaccept
|
|
#
|
|
# A multi-word pattern like {Yes, I accept} therefore never matches, and the
|
|
# wrapper sits there looking like it is not running at all. Match one word.
|
|
|
|
set timeout 90
|
|
log_user 1
|
|
|
|
set answered_theme 0
|
|
set answered_trust 0
|
|
set answered_bypass 0
|
|
|
|
spawn -noecho claude --dangerously-skip-permissions {*}$argv
|
|
|
|
expect {
|
|
-re {Choose} {
|
|
if {!$answered_theme} { set answered_theme 1; send "\r" }
|
|
exp_continue
|
|
}
|
|
-re {trust} {
|
|
if {!$answered_trust} {
|
|
set answered_trust 1
|
|
send_user "\n\[claude-autonomous] accepting the workspace trust prompt\n"
|
|
send "1\r"
|
|
}
|
|
exp_continue
|
|
}
|
|
-re {accept} {
|
|
if {!$answered_bypass} {
|
|
set answered_bypass 1
|
|
send_user "\n\[claude-autonomous] accepting the Bypass Permissions disclaimer\n"
|
|
send "2\r"
|
|
}
|
|
exp_continue
|
|
}
|
|
timeout {
|
|
# No new gate for a while: the session is up (or never had one). Stop
|
|
# matching so nothing later in the run can be answered by accident.
|
|
}
|
|
eof { exit }
|
|
}
|
|
|
|
# Hand the terminal over for the rest of the run.
|
|
interact
|