Skip to content

feat: a herdr pane says which Claude account its agent is running as - #568

Closed
JSmithRobotics wants to merge 4 commits into
blooop:feat/claude-profiles-listfrom
JSmithRobotics:feat/herdr-claude-profile
Closed

feat: a herdr pane says which Claude account its agent is running as#568
JSmithRobotics wants to merge 4 commits into
blooop:feat/claude-profiles-listfrom
JSmithRobotics:feat/herdr-claude-profile

Conversation

@JSmithRobotics

@JSmithRobotics JSmithRobotics commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Top of the stack: #565, #566, #567, then here. Their commits are in this diff
because a cross-fork PR's base must be a branch in this repository. Merge them in
order and this collapses to its own four commits.

Two behaviours, and the second is the one that was actually wrong.

A pane's row says which account the agent in it is running as

Reported to herdr as the display label profile=<name>. A
profile is chosen per launch and forwarded per session, so two tabs side by side
can be two different accounts with nothing on screen to tell them apart -- and the
failure that matters here is not noticing.

Reported under devlaunch's own source name, so it sits beside herdr's own labels
rather than overwriting one, and cleared rather than left stale when a launch
forwards the default login.

A pane opened beside an agent inherits that agent's profile

The pane shell shipped in 0.29.0 already opens in the workspace its tab holds. A
workspace is not an account
, so it read the default login while the agent one
pane over ran as another -- the right container under the wrong identity, which is
the same class of mistake profiles exist to prevent.

It is read from the agent's own argv rather than from a note kept anywhere, for the
reason the pane shell keeps nothing: the argv is what is true, and a record would be
a second copy of it that can go stale.

Two details in that read, both deliberate:

  • An exact element match, and it stops at a bare --. A prompt that contains
    the words --claude-profile work is a prompt.
  • Not gated on the program name. The argv is dl's own transport line either way,
    and gating on it would be a second thing to keep in step with the transport.

Nothing is inherited across a boundary

  • A pane in a tab holding no agent has no account to inherit, and gets the
    default login.
  • The environment of an agent already running is fixed at exec, so switching
    profile reaches the next session and never the one on screen. Worth stating
    because a label makes the opposite look plausible.

Public surface

One row removed and three added, and the removal is the interesting half:
PaneDestination::Workspace stops being a tuple variant and becomes a struct
variant carrying workspace_id and claude_profile.

That is a breaking change to anything constructing or matching it
positionally
, and it lives in the tripwire file rather than the promise file, so
the snapshot is the only place it shows. dl is the sole caller today, which is
exactly why it is worth writing down: nothing else would have said so.

The variant grew a field rather than gaining a sibling because the profile is a
property of the destination, not a different kind of destination.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AdSFnBdxie6TosHVmjLY28

Summary by Sourcery

Show Claude account context in herdr and carry the active agent profile into adjacent panes.

New Features:

  • Display each Claude agent's active profile in herdr pane metadata.
  • Make panes opened beside an agent inherit that agent's Claude profile while retaining the default login when no profile is present.

Bug Fixes:

  • Prevent stale profile labels and prevent adjacent panes from using the wrong Claude account.

Enhancements:

  • Read profiles directly from the live agent launch arguments with exact flag and separator handling.
  • Tolerate metadata-reporting failures without affecting session launches.

Documentation:

  • Document profile display, inheritance boundaries, and default-login behavior in the changelog.

Tests:

  • Add coverage for profile metadata commands, argument parsing, pane association, clearing, and failure tolerance.

Chores:

  • Update the pane destination API to carry the workspace identifier and optional Claude profile together.

@sourcery-ai

sourcery-ai Bot commented Sep 4, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR adds explicit, per-launch Claude account selection backed by externally managed profiles, exposes those profiles and their actual account identities, forwards the selection through dl and aid, and makes herdr and adjacent pane shells reflect the live agent's profile without stale or cross-boundary inheritance. It also fixes host credential lookup for CLAUDE_CONFIG_DIR and records the breaking PaneDestination::Workspace API shape change.

Sequence diagram for per-launch Claude profile forwarding

sequenceDiagram
    participant User
    participant DL as dl
    participant Profiles as ProfileDirectory
    participant Claude as ClaudeSession

    User->>DL: launch --claude-profile work
    DL->>Profiles: read work/.credentials.json
    alt credential found
        Profiles-->>DL: access token
        DL->>Claude: start session with CLAUDE_CODE_OAUTH_TOKEN
    else missing or invalid credential
        Profiles-->>DL: no credential
        DL-->>User: refuse launch
    end
Loading

Sequence diagram for profile reporting and pane inheritance

sequenceDiagram
    participant AgentPane as AgentPane
    participant Herdr
    participant DL as dl
    participant NewPane as AdjacentPane

    DL->>AgentPane: inspect argv for --claude-profile
    AgentPane-->>DL: profile name or none
    DL->>Herdr: report-metadata profile=name
    DL->>Herdr: clear-token profile when no profile
    NewPane->>Herdr: inspect agent pane processes
    Herdr-->>NewPane: workspace and profile
    NewPane->>DL: launch shell with inherited profile
    DL->>NewPane: start shell under selected profile
Loading

File-Level Changes

Change Details Files
Add per-launch Claude profile selection and safe credential resolution across the CLI, launch pipeline, and aid forwarding.
  • Add --claude-profile and --claude-profiles CLI flows, completion, validation, ignored-flag behavior, and account-aware profile listing.
  • Resolve named profiles from external profile directories without writing or falling back; preserve precedence for opt-out, explicit profiles, inherited tokens, and CLAUDE_CONFIG_DIR.
  • Forward the selected profile into sessions and document profile storage, environment precedence, security boundaries, and unsupported contexts.
  • Expose the profile through aid and update the public API/snapshot surface for the new launch data.
rust/dl/src/cli.rs
rust/dl/src/commands.rs
rust/dl/src/launch.rs
rust/aid/src/rewrite.rs
rust/devlaunch-core/src/clients/claude.rs
rust/devlaunch-core/src/domain/xdg.rs
rust/devlaunch-core/src/flows/claude_profiles.rs
rust/devlaunch-core/src/flows/launch.rs
rust/devlaunch-core/src/flows/mod.rs
rust/devlaunch-core/public-api.api.txt
rust/devlaunch-core/public-api.rest.txt
rust/devlaunch-core/completions/dl.bash
Report the active Claude profile in herdr and propagate it to shells opened beside an agent.
  • Set or clear the profile=<name> metadata token under the devlaunch:claude source, tolerating reporting failures.
  • Extract profiles from the agent pane's exact argv, stopping at -- and supporting separate and equals forms without program-name gating.
  • Extend PaneDestination::Workspace with workspace and profile fields so the pane shell inherits the live agent's profile, while tabs without agents use the default login.
  • Add focused tests for metadata commands, argv parsing, pane association, stale-label clearing, and destination behavior.
rust/devlaunch-core/src/clients/herdr.rs
rust/devlaunch-core/src/flows/session_manager.rs
rust/devlaunch-core/src/flows/launch.rs
rust/dl/src/commands.rs
rust/devlaunch-core/public-api.rest.txt
Align host credential discovery with Claude Code's CLAUDE_CONFIG_DIR behavior.
  • Read the host credential from CLAUDE_CONFIG_DIR instead of also consulting ~/.claude when the variable is set, while retaining inherited-token and opt-out precedence.
  • Add coverage for moved configurations, empty or missing directories, and refresh-token exclusion.
  • Update user-facing documentation and changelog guidance.
rust/devlaunch-core/src/clients/claude.rs
CHANGELOG.md
README.md
docs/workspace-tools.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@gitguardian

gitguardian Bot commented Sep 4, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

Since your pull request originates from a forked repository, GitGuardian is not able to associate the secrets uncovered with secret incidents on your GitGuardian dashboard.
Skipping this check run and merging your pull request will create secret incidents on your GitGuardian dashboard.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
- - Generic CLI Secret d900d74 rust/devlaunch-core/src/flows/session_manager.rs View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@JSmithRobotics
JSmithRobotics force-pushed the feat/herdr-claude-profile branch from d0317de to 56d270d Compare September 4, 2026 10:19
@JSmithRobotics
JSmithRobotics changed the base branch from main to feat/claude-profiles-list September 4, 2026 10:19
@JSmithRobotics
JSmithRobotics force-pushed the feat/herdr-claude-profile branch from 56d270d to 1c16315 Compare September 4, 2026 10:20
@JSmithRobotics
JSmithRobotics force-pushed the feat/claude-profiles-list branch from db4d3e3 to c920883 Compare September 4, 2026 10:20
@codecov

codecov Bot commented Sep 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.48555% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.64%. Comparing base (b60c6da) to head (f9cfd93).

Files with missing lines Patch % Lines
rust/dl/src/commands.rs 0.00% 9 Missing ⚠️
rust/devlaunch-core/src/flows/launch.rs 0.00% 4 Missing ⚠️
Additional details and impacted files
Flag Coverage Δ
python 42.98% <ø> (ø)
rust 94.92% <92.48%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
shipped code (rust) 94.92% <92.48%> (+0.01%) ⬆️
harness and tooling (python) 42.98% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

…with

`--herdr-shell` re-enters as `dl <ws>`, and it was passing `claude_profile:
None`. So a session started with `--claude-profile work` got a sibling pane
authenticated as the *default* account, which is the one way this feature could
mislead quietly: the pane looks like the agent's shell and is signed in as
somebody else.

Read from the live sibling rather than remembered, which is what lets it exist
at all. A profile is named per launch and deliberately not stored with the
workspace, so there is nothing on disk to inherit; the running process is the
only record. `pane_destination` is already asking herdr what the tab's panes are
running, so the profile costs no round trip -- `profile_among` reads the same
`PaneProcessInfo` that `workspace_among` does, and reads it off the *same* pane,
so a tab holding two sessions cannot pair one session's workspace with another's
account.

Two properties of the reader are load-bearing, and one case decides both.
`aid <ws> add --claude-profile support to the docs` becomes a dl line whose
prompt is a single argument holding that text, and the ssh transport carries the
same string as one payload argument. Elements are compared whole, so neither is
an argv element equal to `--claude-profile` and neither is read as a flag; and
the scan stops at the first bare `--`, which says it a second way and cheaply. A
real flag before the `--` is still read, so the stop is a stop and not a refusal.

Deliberately not gated on the program name. `dl` on a host may be `dl`,
`dl-next` or an absolute path, and matching that family by prefix is exactly the
fuzzy test that made `ssh_host` necessary -- a sibling pane running
`ssh -F myconf.devpod somehost` once made the pane shell claim a workspace
called `myconf`. The pane-level coupling is the stronger guard: the profile is
asked only of a pane that has already named a workspace, so an unrelated pane is
never consulted.

`PaneDestination::Workspace` becomes a struct variant and moves the public-API
snapshot.
`dl --claude-profiles` names the account behind each profile, but only where
somebody thinks to look. herdr's sidebar is where the panes actually are, and it
had the directory name or nothing.

So a launch that resolved a `Reporting` also reports the profile as a
display-only metadata token, which herdr renders as `$profile` in an agent row.
A token rather than `--display-agent`: that field is the agent name
`report-agent` sets and the one herdr picks a detection manifest by, so writing
a profile into it would corrupt state detection to gain a label.

**Clearing matters as much as setting**, and is why this is called with `None`
too. A pane is reused. A launch naming `work` followed by one naming nothing
would otherwise leave the sidebar asserting an account the running session is
not using -- the exact mislabelling `--claude-profiles` exists to prevent, moved
somewhere more visible and more trusted.

Display only, so it cannot disturb the idle/working/blocked the Claude Code hook
reports through the forwarded socket. And reported then tolerated, like
everything else in this flow: a herdr that is gone, too old for
`report-metadata` or merely slow costs the label and never the session. The
signature carries most of that guarantee, since there is no outcome to branch
on.

Syntax verified against herdr 0.8.2 rather than guessed: the socket-API docs
name the method `pane.report_metadata` and document no CLI wrapper, so herdr was
installed from conda-forge and asked. `pane report-metadata [OPTIONS] --source
<ID> <PANE_ID>`, with `--token NAME=VALUE` and `--clear-token NAME`. The pane id
is given first, matching the `report-agent` invocation in HOOK that was measured
against a live herdr. A guessed argv would have been a feature that never worked
and never said so, since every call in this subsystem ends `>/dev/null 2>&1`.

`devlaunch:claude` was a bare literal in HOOK and is now a named constant with
the diff test the standing rule asks for.
One row removed and three added, and the removal is the interesting half:
`PaneDestination::Workspace` stops being a tuple variant and becomes a struct
variant carrying `workspace_id` and `claude_profile`.

That is a breaking change to anything that constructs or matches it positionally,
and it is in the tripwire file rather than the promise file, so the snapshot is
the only place it shows. It is in-tree only today -- `dl` is the sole caller --
which is exactly why it is worth writing down: nothing else would have said so.

The variant grew a field rather than gaining a sibling because the profile is a
property of the destination, not a different kind of destination: a pane opened
beside an agent inherits the account that agent is running as, and a pane opened
anywhere else has no account to inherit.

Regenerated with `scripts/public-api-snapshots.sh` on nightly 1.100.0
(2026-09-03) and cargo-public-api 0.52.0, the pin the script names.
Two behaviours, and the second is the one worth the words: a pane opened beside
an agent inherits that agent's account, where before it opened in the right
workspace under the wrong login. A workspace is not an account.
@JSmithRobotics
JSmithRobotics force-pushed the feat/herdr-claude-profile branch from 1c16315 to f9cfd93 Compare September 4, 2026 10:26
@JSmithRobotics
JSmithRobotics force-pushed the feat/claude-profiles-list branch from c920883 to b60c6da Compare September 4, 2026 10:26
@JSmithRobotics

Copy link
Copy Markdown
Collaborator Author

Superseded by #573, which is the same branch opened from this repository rather than from a fork.

Moved because a cross-fork pull request's base has to be a branch in the base repository, so the stack needed its parents pushed here anyway -- which left three branch names living on two remotes, two of them serving as one PR's head and another's base at the same time. Every update then had to reach both remotes or a diff would quietly misrepresent itself. Same commits, one ref each, and #570 to #573 are now a native GitHub stack.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant