gitbutlerapp/gitbutler · error

HEAD is detached

Error message

HEAD is detached

What it means

current_branch_targets in crates/but-agentlog/src/environment.rs resolves the current branch from HEAD to build ObservedTargets. If repo.head() returns a HEAD that is not a symbolic reference (no referent), it bails with "HEAD is detached" because branch targets cannot be determined from a detached HEAD.

Solutions

  1. Check out a local branch first: `git switch <branch>` (or `git switch -c <branch>` to keep the commits).
  2. Skip the branch-target snapshot when HEAD is detached: test with `git symbolic-ref -q HEAD` before invoking.
  3. Extend the caller to handle detached HEAD explicitly (record the commit id instead of a branch target).

Example fix

// before
but agentlog ...  # fails in detached HEAD
// after
git switch -c work-from-detached
but agentlog ...
Defensive patterns

Strategy: fallback

Validate before calling

# detect detached HEAD before running
git symbolic-ref -q HEAD >/dev/null 2>&1 || { echo "HEAD is detached"; exit 1; }

Try / catch

match result {
    Err(e) if e.to_string().contains("HEAD is detached") => {
        // fall back to commit-id-based snapshot or skip
    }
    r => r?,
}

Prevention

When it happens

Trigger: Calling current_branch_targets (via workspace_snapshot_with_current_branch_fallback) while the repo is in detached-HEAD state, e.g. after `git checkout <sha>` or a rebase in progress.

Common situations: Agent tooling runs inside a CI checkout pinned to a commit; a user checked out a tag or commit for inspection; mid-rebase states leave HEAD detached.

Understand the failure class

Background: "git command failed": what it means when a tool shells out to git and git exits non-zero — this error's family across 21 libraries.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@58e5313667 (2026-09-18). Data as JSON: /api/errors/112196a0785f220f. Report an issue: GitHub.

Appendix: source

Thrown at crates/but-agentlog/src/environment.rs:385

                observation.observed_targets = fallback;
            }
            Ok(observation)
        }
        Err(error) => match current_branch_targets(repo) {
            Ok(fallback) if !fallback.is_empty() => Ok(WorkspaceObservation {
                stacks: Vec::new(),
                observed_targets: fallback,
                error_kind: Some(SnapshotErrorKind::Workspace),
            }),
            _ => Err(error),
        },
    }
}

fn current_branch_targets(repo: &gix::Repository) -> anyhow::Result<ObservedTargets> {
    let head = repo.head()?;
    let Some(ref_name) = head.referent_name() else {
        anyhow::bail!("HEAD is detached");
    };
    let full_ref_name = ref_name.to_string();
    if !full_ref_name.starts_with("refs/heads/") {
        anyhow::bail!("HEAD does not point to a local branch");
    }
    if is_workspace_ref_name(ref_name) {
        anyhow::bail!("HEAD points to the GitButler workspace branch");
    }
    let branch = BranchTarget {
        key: format!("ref:{full_ref_name}"),
        name: ref_name.shorten().to_string(),
    };
    // Derive the PR association from the forge review cache rather than stored
    // metadata. Synthesize a metadata entry carrying the resolved number so a
    // branch with a cached PR is reported even without stored branch metadata.
    let reviews = match resolve_branch_pr(repo, ref_name, &forge_prs_by_head(repo)) {
        Some(pull_request) => {
            let mut metadata = Branch::default();

View on GitHub (pinned to 58e5313667)