Troubleshooting
Permission denied writing LaunchAgents plist
When running loop daemon:start, you may see:
Error: writing plist: open /Users/<user>/Library/LaunchAgents/com.loop.agent.plist: permission deniedThis happens when ~/Library/LaunchAgents/ or the plist file is owned by root instead of your user. Deleting or writing the plist also requires write permission on the directory itself. Fix both by reclaiming ownership:
sudo chown $(whoami):staff ~/Library/LaunchAgents
sudo chown $(whoami):staff ~/Library/LaunchAgents/com.loop.agent.plistThen retry loop daemon:start.
Docker build fails with TLS certificate errors
When running loop serve, the Docker image build may fail with:
E: Failed to fetch https://deb.debian.org/debian/dists/.../Release.gpg Server certificate verification failed. CAfile: ...
E: Some index files failed to download.This occurs on corporate networks where a proxy or firewall (e.g. Palo Alto, Zscaler, Netskope) performs SSL inspection using a custom CA certificate that Docker containers don’t trust.
Fix: inject corporate CA certificates into the build
Export your corporate CA certificates from the macOS system keychain:
security find-certificate -a -p /Library/Keychains/System.keychain > ~/.loop/container/corporate-ca.pemPatch
~/.loop/container/Dockerfileto inject the certificates before any network operations. Add these lines after eachFROMline and before anyRUN apt-getorRUN curlcommands:COPY corporate-ca.pem /usr/local/share/ca-certificates/corporate-ca.crt RUN update-ca-certificatesFor example, the builder stage becomes:
FROM golang:1.27 AS builder COPY corporate-ca.pem /usr/local/share/ca-certificates/corporate-ca.crt RUN update-ca-certificatesApply the same pattern to the final stage and to
~/.loop/container/chrome.Dockerfile(note:chrome.Dockerfileis still Alpine — use thecat … >> /etc/ssl/certs/ca-certificates.crtform there).Retry
loop serve.
The agent image sets AWS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt, because the AWS CLI checks TLS against its own CA list rather than the system store; with the CA added as above, aws stops failing with CERTIFICATE_VERIFY_FAILED. Set envs.AWS_CA_BUNDLE to point it elsewhere.
Note: When
loop serveships a new version of the embedded container files, your edited~/.loop/container/Dockerfile(andchrome.Dockerfile) is preserved asDockerfile.bkp(andchrome.Dockerfile.bkp) before being overwritten. Diff the.bkpagainst the refreshed file to re-apply your local changes.
Daemon not healthy after daemon:start
The desktop app polls GET /api/health for ~15 seconds after launching the daemon; if it never turns healthy you’ll see a “daemon not running” state.
- Check the log — the daemon writes to
~/.loop/loop.logby default (configurable vialog_file). Startup failures (bad config, port in use, Docker unreachable) land there with the reason. - Run it in the foreground —
loop serveruns the same daemon attached to your terminal, which surfaces config errors immediately.daemon:startis the supervised background variant (LaunchAgents on macOS);loop daemon:statusshows whether it’s loaded. - Common causes: another process bound to the API port (
api_addr, default:8222), a malformed~/.loop/config.json(it’s HJSON — comments are fine, but unbalanced braces are not), or Docker not running (the daemon starts without Docker but container features will error).
Security gate: command blocked or approval prompt never resolves
The seccomp gate (gates.agentgate) is enabled by default and runs inside every agent container. Most operations are allowed silently; credential-adjacent writes and container-escape shapes either deny or prompt.
- A prompt is waiting somewhere you’re not looking. Approval cards render in the chat for chat-initiated work, and as an overlay on the specific terminal pane for terminal-initiated work. The blocked syscall waits until you resolve the card — an agent that seems “stuck” often has a pending approval on another surface. The Audit panel lists recent gate decisions.
- A legitimate operation keeps getting blocked. Decisions come from first-match-wins rules; the fall-through is
gates.agentgate.default_decision("allow"by default). Add a project-level rule for the specific path/command rather than loosening globals: project rules come before the global approves and allows, so a narrowallowthere wins over a broader global one. A deny you wrote in the global config always wins; an exception to it belongs there. Loop’s default credential denies (~/.aws,~/.kube,~/.ssh, …) are the exception: a projectallowfor them works once you trust the project config. If it has no effect, check that the config is trusted and unchanged since (Settings shows a “Project config changed” notice). - Kill switch. Set
gates.agentgate.enabled: falsein the global config. The project setting is ignored, so a project can neither switch the gate off nor back on. Disabling the gate also disables the Docker proxy. Containers created before a config change keep their old policy — recreate them to pick up new rules.
Docker proxy blocks an agent’s Docker command
When the gate is enabled, agents talk to a filtered Docker socket (loop dockerproxy), not the real daemon. Denied calls return 403 inside the container; approve-rule matches block until you resolve the approval card.
- Check the Audit panel to see which rule matched.
- Add or adjust
gates.docker_proxy.http_rulesorbody_rulesfor what’s being blocked. Project rules may use any decision but come after the global denies;gates.docker_proxy.enabledis set globally only.
Messages you may see from docker (or a library) inside the agent, and what to do:
| Message | Cause | Fix |
|---|---|---|
bind source <path>: … (sources are checked before the container is created, so they must exist) (400) | The proxy resolves every bind source before the create, so -v ./dir:/x no longer creates a missing dir | Create the directory first (mkdir -p dir) |
binds under the agent's mounts need Docker API >= 1.45 or docker socket mounts need Docker API >= 1.45 (400) | The client pins an older API (DOCKER_API_VERSION, an old SDK); pinned binds and the nested socket use VolumeOptions.Subpath | Unset the pin or raise it to 1.45+ |
container-escape risk: bind-mount or flag rejected (403) | A baseline deny matched: a system-path bind, --privileged, a host namespace, a --cap-add outside the allowlist, a non-runc runtime, … | Check the Audit panel for the field; see Gates: Default policy
. Add a project body_rules allow only if you accept the risk |
Approval card: bind mount outside the agent's own mounts | A host path the agent doesn’t mount read-write (or a read-write bind of one it mounts read-only), e.g. /tmp, a sibling project | Approve it, or bind a path inside the workspace instead |
ambiguous request body: keys differ only in case (400) | The body has e.g. both HostConfig and hostconfig | Fix the client; the daemon would keep one of them depending on key order |
container create body too large (413) | Create bodies over 1 MiB can’t be inspected | Move large env/labels into files |
volume names starting with loop-bind- are reserved for the docker proxy (403) | A request mounts or creates a loop-bind-* volume by name | Use a different volume name |
docker socket mounts are unavailable: the nested proxy socket is not configured (403) | The proxy couldn’t set up its second socket at startup | Recreate the agent container; creates without a socket mount still work |
Docker Desktop’s volume list shows loop-bind-<hash> volumes: those back pinned binds of containers the agent started (see Binds are pinned to the agent’s mounts
). Loop removes the unused ones whenever it removes a container; docker volume prune -a --filter label=app=loop-bind removes them by hand (-a, since they are named volumes).
Run fails with “Docker is out of disk space”
The disk that fills is the Docker VM’s (Docker Desktop’s virtual disk), not
the host’s, so freeing host space doesn’t help. Container create, file copies
and the agent’s own writes fail with no space left on device, and the chat
shows the notice instead of a reply. Free space inside Docker, then send the
message again:
docker builder prune -a– the build cache is usually the largest share.docker image prune -aanddocker container prune– unused images and stopped containers.docker system dfshows what is using the space.
See Run Failures for how other run errors show.
Agent fails with “No conversation found with session ID”
Claude Code prunes transcripts under ~/.claude/projects after
cleanupPeriodDays (30 by default), while Loop pins a channel’s session id
until a run succeeds — so a channel idle for longer than the window points at a
file that is gone, and every turn fails on --resume.
Loop now detects the missing transcript and starts a fresh session instead (agent runs, terminal panes, and new worktrees alike; see Pruned transcripts ), so this should self-correct. If you hit it on an older build, or you want the conversation back rather than a fresh start:
- Recover the transcript if a copy survives. Worktrees get their own copy
under
~/.claude/projects/<encoded-worktree-path>/, and those copies are pruned on their own schedule —ls -t ~/.claude/projects/*/<session-id>.jsonloften turns one up. Copy it back into the channel’s own project dir (the dir path with/and.replaced by-). - Or start clean — clear the channel’s stored session from the Sessions panel (+ New) and let the next turn open a new one.
- Keep transcripts longer by raising
cleanupPeriodDaysin~/.claude/settings.json.
Memory search returns nothing / Ollama issues
Semantic memory is off by default — search_memory silently returns no results until you set memory.enabled: true in the global config and configure memory.paths.
When enabled, the first Embed() call lazily starts a local loop-ollama Docker container (port 11434, model nomic-embed-text, cached in the loop-ollama named volume), and the container auto-stops after ~5 minutes idle. Failure modes:
- Port conflict — if something else (e.g. a native Ollama install) is already bound to
11434, the container can’t start. Stop the other instance or pointmemory.embeddingsat it instead of the managed container. - First-use model pull is slow — the initial
nomic-embed-textdownload happens inside the container on first index/search; on corporate networks it’s subject to the same TLS-inspection issues as the image build above. Subsequent runs use the named volume. - Indexing lag — re-indexing runs at startup and then on a ticker (default 5 minutes,
memory.reindex_interval_sec); files edited moments ago may not be searchable yet.