gitbutlerapp/gitbutler · warning
HEAD points to the GitButler workspace branch
Error message
HEAD points to the GitButler workspace branch
What it means
current_branch_targets resolves the repository's HEAD to a local branch target for the agentlog environment snapshot. GitButler manages a special 'workspace' ref that holds virtual-branch state; HEAD on that ref is not a real local branch the caller can report, so the function bails with this error. It is an intentional guard: the caller (workspace_snapshot_with_current_branch_fallback) uses the bail to decide the repo is in GitButler-managed mode rather than on a plain branch.
Solutions
- Detach from or switch to a normal branch (git switch <feature-branch>) so HEAD no longer points at the GitButler workspace ref
- Deactivate GitButler's workspace management for the repo (quit/unapply in the GitButler UI or `but` CLI) before snapshotting
- Treat this error in the caller as an expected signal that the repo is GitButler-managed and skip plain-branch snapshotting rather than surfacing it as a failure
- Check `git symbolic-ref HEAD` first and only run current_branch_targets when the ref is not the GitButler workspace ref
Example fix
// before
let targets = workspace_snapshot_with_current_branch_fallback(&repo)?;
// after
match workspace_snapshot_with_current_branch_fallback(&repo) {
Ok(t) => t,
Err(e) if e.to_string().contains("GitButler workspace branch") => {
// repo is GitButler-managed; skip plain-branch snapshot
ObservedTargets::default()
}
Err(e) => return Err(e),
} Defensive patterns
Strategy: fallback
Validate before calling
let head_ref = repo.head()?.referent_name().map(|r| r.to_string());
let is_gitbutler_workspace = head_ref
.as_deref()
.map(is_workspace_ref_name)
.unwrap_or(false);
if is_gitbutler_workspace { /* skip plain-branch snapshot path */ } Type guard
fn is_plain_local_branch(repo: &gix::Repository) -> Option<String> {
let name = repo.head().ok()?.referent_name()?.to_string();
(name.starts_with("refs/headers/") || name.starts_with("refs/heads/"))
.then_some(name)
.filter(|n| !is_workspace_ref_name_short(n))
} Try / catch
match current_branch_targets(repo) {
Ok(targets) => targets,
Err(e) if e.to_string().contains("GitButler workspace branch") => {
// expected: GitButler-managed repo; use workspace snapshot path
ObservedTargets::default()
}
Err(e) => return Err(e),
} Prevention
- Check HEAD's symbolic ref before resolving branch targets and skip GitButler workspace refs
- Handle this bail as a signal to use the GitButler workspace snapshot path instead of treating it as a failure
- Test snapshotting in repos where GitButler is active to catch this early
- Keep GitButler workspace state deactivated in CI fixtures unless explicitly testing it
When it happens
Trigger: Calling workspace_snapshot_with_current_branch_fallback (via current_branch_targets) on a repository where HEAD points to refs/heads/<gitbutler workspace branch> (is_workspace_ref_name returns true) — i.e. the GitButler workspace/apply flow has checked out its managed ref.
Common situations: Developer has GitButler installed and its workspace is active in the repo; running agentlog snapshotting while GitButler holds HEAD; mixing plain-git tooling with GitButler-managed repos; GitButler left the workspace ref checked out after an interrupted session.
Understand the failure class
Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.
Related errors
- Cannot delete GitButler technical branch
- Invalid response
- a committed transaction always materializes a workspace
- Aborting due to empty branch name
- Ad-hoc (single-branch) branch moves are not supported…
AI-assisted analysis of gitbutlerapp/gitbutler@58e5313667 (2026-09-18).
Data as JSON: /api/errors/99ff5422022c6b13.
Report an issue: GitHub.
Appendix: source
Thrown at crates/but-agentlog/src/environment.rs:392
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();
metadata.review.pull_request = Some(pull_request);
review_targets(&branch.key, Some(&metadata))
}
None => Vec::new(),
};
let mut observed_targets = ObservedTargets {
branches: vec![branch],View on GitHub (pinned to 58e5313667)