Skip to content

herdr cannot see an agent inside a dl workspace: the hook arrives, its socket, binary and pane id do not #547

Description

@blooop

The gap

An agent launched by aid is invisible to every host-side session manager, and
the reason is structural rather than a missing feature in any of them.

herdr classifies agent state two ways (docs):

  1. Screen and process detection — it matches rules against the pane's
    terminal output and the foreground process name.
  2. Lifecycle hooks, which are "authoritative when installed and actively
    reporting for the running pane".

Under dl/aid the host pane's foreground process is devpod ssh and its
output is an ssh stream, so (1) has no claude to find. That leaves (2), and
(2) is where this gets interesting: the hook script arrives in the container
and fires, and then has nothing to talk to.

herdr installs ~/.claude/hooks/herdr-agent-state.sh, and the hook reports over
a unix socket (docs):

"$HERDR_BIN_PATH" pane report-agent "$HERDR_PANE_ID" \
  --source custom:my-agent --agent my-agent --state working

with HERDR_BIN_PATH, HERDR_PANE_ID, HERDR_SOCKET_PATH and HERDR_ENV=1
set in the pane's environment.

Now cross that with what this repo already does. .devcontainer/claude-code/devcontainer-feature.json:23
mounts ~/.claude/hooks into the container read-only:

source=${localEnv:HOME}/.claude/hooks,target=/home/vscode/.claude/hooks,type=bind,readonly

So for any repo shipping the claude-code feature, herdr-agent-state.sh is
present inside the container and Claude Code runs it. Three of the four things it
needs are absent:

  • HERDR_BIN_PATH names a host path, so it does not resolve in the container.
  • HERDR_SOCKET_PATH names a host unix socket, unreachable across the
    container's mount namespace.
  • HERDR_PANE_ID and HERDR_ENV are not forwarded by dl at all.

The failure is silent, which is what makes it expensive: the hook runs, cannot
report, and herdr falls back to screen detection, which is looking at ssh. This
is the same shape as the $ZELLIJ guard in the dotfiles Notification/Stop
hooks — a reporting path that no-ops precisely in the workspaces that are the
whole point of dl.

Why this is worth dl's attention rather than herdr's

Nothing herdr can do fixes it from its side. It cannot reach into a container it
did not create, and the container is created by dl. Meanwhile dl already owns
every mechanism the fix needs, for exactly this class of problem:

So the shape of the change is already established here. What is not established
is whether the transport survives the boundary at all, and that has to be measured
before any of it is built.

Measure first: three unknowns, none of them checked

I could not run these — no herdr, no devpod and no docker in the environment this
was written from. This is the part that wants an agent on the machine, and the
build below should not start until all three are answered.

Prerequisites: herdr installed and running, herdr integration install claude
done, one real workspace, docker available.

  1. Is HERDR_SOCKET_PATH stable, or per-run?

    herdr ... # inside a herdr pane
    echo "$HERDR_SOCKET_PATH" "$HERDR_PANE_ID" "$HERDR_BIN_PATH"

    Restart herdr and compare. This decides whether the fix is possible as
    stated.
    A bind mount only lands at container creation
    (docs/workspace-tools.md, "Existing workspaces"), so a socket path that
    changes per herdr run cannot be served by a mount declared at create time, and
    the design has to change to something reachable at attach: a stable
    well-known path herdr is pointed at, or a forwarding proxy, or a different
    transport entirely.

  2. Does the socket accept a peer from another namespace and another uid?
    Mount it into a container by hand and report against it:

    docker run --rm -it \
      -v "$HERDR_SOCKET_PATH:/tmp/herdr.sock" \
      -v "$(command -v herdr):/usr/local/bin/herdr:ro" \
      ubuntu:24.04 \
      env HERDR_SOCKET_PATH=/tmp/herdr.sock \
        herdr pane report-agent "<pane id>" --source custom:probe --agent claude --state working

    Then check whether that pane's state changed in the herdr UI. The risk worth
    naming: if herdr validates the peer with SO_PEERCRED, a container user whose
    uid differs from the host user's (vscode is uid 1000, the host user may not
    be) is refused. A refusal here is not fatal to the ticket but changes it from
    a mount to a proxy.

  3. Is reporting against the host pane id the right semantics?
    The pane running devpod ssh is where the agent appears to a human, so
    reporting the container agent's state against it looks correct, and going to
    that row goes to the right place. Confirm herdr does not overwrite it a moment
    later from screen detection of the ssh process — i.e. that "authoritative when
    installed and actively reporting" beats the screen rules for a pane whose
    visible process is not an agent at all. If it does not, HERDR_AGENT=<agent>
    (documented for "VMs and wrappers") may be the missing half, and dl would
    set it host-side rather than in the container.

Record the answers on this issue before opening a PR. If (1) says per-run, close
this and open a design ticket instead: the rest of the plan is void.

The build, if the measurements allow it

One consent variable in the established style, off by default:

Variable What it does
DEVLAUNCH_HERDR=1 Lend herdr into the workspace, bind-mount the reporting socket, and rewrite HERDR_BIN_PATH/HERDR_SOCKET_PATH to their container paths while forwarding HERDR_PANE_ID and HERDR_ENV

A consent and not a denial, for the reason DEVLAUNCH_ZELLIJ's doc comment gives:
it costs a mount and a lend, so the default must be that no launch pays for it.
Read it through provisioning_disabled's shared parse so a value spelled the same
way cannot answer differently here than in the other switches.

Two consequences to state in the docs rather than discover:

  • This one needs recreate, not up. The zellij stage lands on a setup pass,
    so dl <ws> up is enough for it. A bind mount is not, so an existing workspace
    needs dl <ws> recreate — the more expensive verb the zellij page explicitly
    says buys nothing there, and the only thing that works here.
  • Do not build on DEVLAUNCH_WORKSPACE_ID. launch.rs:135 is explicit that it
    exists for a project's host-side initializeCommand; it is not in the
    container's environment. Inside, the workspace id is the hostname
    (provision.rs:546).

Scope: herdr only. It is the one manager whose hook protocol is a documented CLI
call over a local socket, so it is the one that can be reached this way at all.
Claude Code's own Agent View is documented as local-only
("Cannot manage remote, cloud, or containerized sessions") and every tmux-and-worktree
manager infers state from host processes, so neither is fixable from this side.

Acceptance

  • The three measurements are recorded on this issue, with the commands run and
    their output.
  • With DEVLAUNCH_HERDR=1, an aid session in a container shows as working
    and then needs input in herdr's agents tab, and selecting its row lands on
    the pane holding that workspace.
  • With the variable unset, no invocation changes meaning and no launch pays for a
    mount or a lend. Pinned by a test, the way the zellij switch is.
  • A stale DEVLAUNCH_HERDR=0 in a profile turns nothing on.
  • docs/workspace-tools.md gains the capability beside the zellij one, including
    the recreate requirement above. Note that flows::provision::lending_contract
    reads that file and matches on headings, so a new section moves with its guard.
  • The README's environment-variable table names the variable
    (test_readme_cli_doc.py), and neither the README nor the new docs prose uses
    an em or en dash (test_docs_prose.py).
  • No second hand-maintained copy of the variable's name or semantics without a
    test beside it diffing the two, per the standing rule in CLAUDE.md.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions