Skip to content

Implment --bare flag on branch naming and commit message generation claude cli invocations#978

Merged
acreeger merged 3 commits intomainfrom
feat/issue-977__bare-flag
Apr 2, 2026
Merged

Implment --bare flag on branch naming and commit message generation claude cli invocations#978
acreeger merged 3 commits intomainfrom
feat/issue-977__bare-flag

Conversation

@acreeger
Copy link
Copy Markdown
Collaborator

Fixes #977

Implment --bare flag on branch naming and commit message generation claude cli invocations

Issue details

This one is a long shot due to OAUTH issues. If env.ANTHROPIC_API_KEY is undefined, we will have to use the --settings flag to configure apiKeyHelper to return the Oauth token, and if it fails, just fall back to non --bare mode.

From docs:

--bare Minimal mode: skip hooks, LSP, plugin sync, attribution, auto-memory, background prefetches, keychain reads, and CLAUDE.md auto-discovery. Sets CLAUDE_CODE_SIMPLE=1. Anthropic auth is strictly ANTHROPIC_API_KEY or apiKeyHelper via --settings (OAuth and keychain are never read). 3P providers (Bedrock/Vertex/Foundry) use their own credentials. Skills still resolve via /skill-name. Explicitly provide context via: --system-prompt[-file], --append-system-prompt[-file], --add-dir (CLAUDE.md dirs), --mcp-config, --settings, --agents, --plugin-dir.


This PR was created automatically by iloom.

@acreeger
Copy link
Copy Markdown
Collaborator Author

acreeger commented Mar 30, 2026

Enhancement Analysis - Issue #977

  • Fetch and review issue details and comments
  • Read project documentation (CLAUDE.md, README.md)
  • Analyze requirements and identify gaps
  • Produce structured enhancement specification

Status: Complete


Enhancement Analysis

Questions for Reporter

Question Answer
Are there other Claude CLI invocations beyond branch naming and commit message generation that should also use --bare? For example, the validation-fix Claude launch in il finish, or the il add-issue enhancement step? @acreeger -- Assumed: No, this is scoped only to branch naming (BranchNamingService with Claude strategy) and commit message generation (in il commit / il finish). These are the lightweight, single-prompt invocations where --bare makes the most sense.
Should --bare mode be the unconditional default for these invocations, or should it be opt-in via a setting (e.g., useBareMode: true)? Assumed: It should be the default behavior for these invocations since they are simple text generation tasks that don't need hooks, LSP, plugins, or CLAUDE.md auto-discovery. The fallback to non-bare mode when auth fails is the safety net.
For the OAuth fallback: when ANTHROPIC_API_KEY is not set and the apiKeyHelper approach via --settings also fails, should the fallback be silent (just use non-bare mode) or should it log a warning so users know they could get faster performance by setting an API key? Assumed: Log a debug-level message (visible with verbose/debug logging) but do not warn the user in normal output. The fallback should be seamless.

Problem Summary

iloom currently invokes the Claude CLI in full mode for lightweight tasks like generating branch names and commit messages. These invocations pay the full startup cost (hooks, LSP, plugin sync, CLAUDE.md discovery, etc.) even though they only need simple text generation. The --bare flag on the Claude CLI would make these invocations significantly faster by skipping all unnecessary initialization.

User Impact

All iloom users experience unnecessary latency during il start (branch naming with Claude strategy) and il commit / il finish (commit message generation). Users on slower machines or with complex project setups (many plugins, large CLAUDE.md files) feel this overhead most acutely.

Enhancement Goal

Add the --bare flag to Claude CLI invocations used for branch naming and commit message generation, reducing startup overhead for these short-lived, simple text generation tasks. Because --bare changes auth behavior (requires ANTHROPIC_API_KEY or apiKeyHelper -- OAuth/keychain are never read), the implementation must detect when ANTHROPIC_API_KEY is unavailable and either configure apiKeyHelper via --settings to provide the OAuth token, or gracefully fall back to non-bare mode.

Next Steps

  • Reporter to confirm scope (which invocations) and fallback preferences
  • Technical analysis to identify all Claude CLI invocation points for branch naming and commit message generation
  • Technical analysis to determine how to detect the current auth method and whether apiKeyHelper can retrieve the OAuth token
  • Implementation planning and execution

Complete Context and Details (click to expand)

Background from Issue Description

The issue author notes this is "a long shot due to OAUTH issues," indicating awareness that the --bare flag's auth constraints may make it incompatible with some user setups. The --bare flag documentation states:

Minimal mode: skip hooks, LSP, plugin sync, attribution, auto-memory, background prefetches, keychain reads, and CLAUDE.md auto-discovery. Sets CLAUDE_CODE_SIMPLE=1. Anthropic auth is strictly ANTHROPIC_API_KEY or apiKeyHelper via --settings (OAuth and keychain are never read). 3P providers (Bedrock/Vertex/Foundry) use their own credentials.

Key Auth Considerations

  • Users with ANTHROPIC_API_KEY set in their environment: --bare works directly
  • Users authenticating via OAuth (the default Claude Code auth): --bare will not read OAuth tokens from the keychain. The workaround is to use --settings with an apiKeyHelper that returns the OAuth token
  • If the apiKeyHelper approach fails (token expired, helper not configured), the invocation must fall back to non-bare mode to avoid breaking the workflow

Affected Features

  1. Branch naming (Claude strategy): When branchNamingStrategy is set to claude (as opposed to simple), iloom invokes Claude CLI to generate a descriptive branch name from the issue title/body. This happens during il start.

  2. Commit message generation: When running il commit or il finish without a -m flag, iloom invokes Claude CLI to generate a commit message from the staged diff and issue context.

Both are short-lived, single-prompt invocations that produce a few words or sentences of output. They do not need code analysis tools, LSP integration, plugins, or CLAUDE.md context.

Additional Context

  • No existing comments on the issue -- no prior discussion or answers to incorporate
  • The issue is authored by the project maintainer, suggesting this is a self-identified optimization opportunity
  • The CLAUDE_CODE_SIMPLE=1 environment variable set by --bare was recently removed from another context (see commit 308e55c: "fix(auth): remove CLAUDE_CODE_SIMPLE env var that breaks OAuth auth"), indicating prior experience with auth complications in simplified modes

@acreeger
Copy link
Copy Markdown
Collaborator Author

acreeger commented Mar 30, 2026

Analysis Phase

  • Identify all Claude CLI invocation points for branch naming and commit message generation
  • Trace the --bare flag data flow through the architecture
  • Research Claude CLI --bare flag behavior and auth implications
  • Analyze OAuth/API key detection and fallback strategy
  • Document affected files and interfaces
  • Write final analysis

Executive Summary

This issue adds --bare flag support to the two lightweight Claude CLI invocations used for branch naming and commit message generation. The --bare flag provides faster startup by skipping hooks, LSP, plugins, and CLAUDE.md discovery. However, --bare sets CLAUDE_CODE_SIMPLE=1, which was explicitly removed from these exact call sites in commit 308e55c (2026-03-22) because it broke OAuth authentication. The implementation must detect whether ANTHROPIC_API_KEY is available and fall back to non-bare mode when it is not, since --bare disables OAuth/keychain credential lookup.

Questions and Key Decisions

Question Answer
Should --bare be the default for these utility calls, or opt-in via settings? Default on with auto-fallback. These are simple text-generation tasks where bare mode is always preferred for speed. The fallback handles the auth case.
How should API key presence be detected? Check process.env.ANTHROPIC_API_KEY at the call sites before passing bare: true to launchClaude. If undefined, omit --bare.
Should --settings with apiKeyHelper be supported as the issue suggests? This adds significant complexity (generating a temp settings file, configuring apiKeyHelper to return an OAuth token). Given the issue says "if it fails, just fall back to non --bare mode," the simpler approach of just checking the env var and skipping bare mode is preferable as a first pass.

HIGH/CRITICAL Risks

  • OAuth auth breakage with --bare: --bare sets CLAUDE_CODE_SIMPLE=1, which disables OAuth credential lookup. Users without ANTHROPIC_API_KEY (the majority who use Claude CLI's default OAuth flow) will get auth errors. The fallback detection is not optional -- it is required to avoid a regression of the bug fixed in 308e55c.

Impact Summary

  • 2 files requiring modification for core feature (src/utils/claude.ts, src/lib/CommitManager.ts)
  • 1 interface to extend (ClaudeCliOptions in src/utils/claude.ts)
  • 2 test files requiring updates (src/utils/claude.test.ts, src/lib/CommitManager.test.ts)
  • Key decision: Whether to attempt the --settings apiKeyHelper approach or keep it simple with env var detection

Complete Technical Reference (click to expand for implementation details)

Problem Space Research

Problem Understanding

Branch naming (generateBranchName) and commit message generation (generateClaudeCommitMessage) invoke claude -p (headless mode) for simple text generation. These calls currently launch the full Claude Code environment (hooks, LSP, plugins, CLAUDE.md discovery), adding unnecessary startup overhead. The --bare flag skips all of this for faster execution.

Architectural Context

Both invocations flow through a single function: launchClaude() in src/utils/claude.ts:151. This function builds CLI args from ClaudeCliOptions and calls execa('claude', args, ...). Adding --bare requires:

  1. Adding bare?: boolean to ClaudeCliOptions
  2. Adding arg-building logic in launchClaude()
  3. Passing bare: true (conditionally) from the two call sites

Historical Context

  • Commit 308e55c (2026-03-22): Removed env: { CLAUDE_CODE_SIMPLE: '1' } from both generateBranchName() and generateClaudeCommitMessage() because it broke OAuth auth. The commit message: "CLAUDE_CODE_SIMPLE=1 disables OAuth credential lookup in Claude CLI, causing 'Not logged in' errors for branch name and commit message generation when no ANTHROPIC_API_KEY is set."
  • The --bare flag per the docs explicitly "Sets CLAUDE_CODE_SIMPLE=1" -- so using --bare reintroduces the same auth problem that was fixed.

Edge Cases Identified

  • No ANTHROPIC_API_KEY set (OAuth users): --bare will fail. Must fall back to non-bare mode.
  • ANTHROPIC_API_KEY set but invalid/expired: --bare will fail with an auth error rather than an OAuth fallback. This is acceptable since the existing Claude error handling in both call sites already catches and falls back gracefully.
  • Mixed auth environments: Some environments may have ANTHROPIC_API_KEY set but rely on OAuth for the actual auth. Checking the env var presence is a reasonable heuristic.

Codebase Research Findings

Affected Area: Branch Name Generation

Entry Point: src/utils/claude.ts:613-669 - generateBranchName() function
Claude invocation: Line 648-652 - calls launchClaude(prompt, { model, headless: true, noSessionPersistence: true })
Dependencies:

  • Uses: launchClaude(), detectClaudeCli(), isValidBranchName()
  • Used By: ClaudeBranchNameStrategy.generate() at src/lib/BranchNamingService.ts:34
  • Used By: LoomManager.createLoom() at src/lib/LoomManager.ts:688
  • Used By: CLI test commands at src/cli.ts:1866,1900,1968

Affected Area: Commit Message Generation

Entry Point: src/lib/CommitManager.ts:299-430 - generateClaudeCommitMessage() private method
Claude invocation: Lines 340-355 - calls launchClaude(prompt, { headless: true, addDir: worktreePath, model: 'claude-haiku-4-5-20251001', timeout: 120000, appendSystemPrompt: '...', noSessionPersistence: true })
Dependencies:

  • Uses: launchClaude(), detectClaudeCli() from src/utils/claude.ts
  • Used By: CommitManager.commitChanges() at src/lib/CommitManager.ts:76
  • Used By: commit command at src/commands/commit.ts
  • Used By: finish command at src/commands/finish.ts

Affected Area: launchClaude() Core Function

Entry Point: src/utils/claude.ts:151-547 - launchClaude() function
Interface: ClaudeCliOptions at src/utils/claude.ts:56-83
Arg building section: Lines 158-243 - builds args[] from options, where --bare would be added
Environment: Line 246 - const claudeEnv = { ...process.env, CLAUDECODE: '0' } - env var passthrough

Similar Patterns Found

  • noSessionPersistence option: Added as boolean to ClaudeCliOptions:75, arg built at line 241-243 (args.push('--no-session-persistence')). The --bare flag follows the exact same pattern.
  • effort option: Added at ClaudeCliOptions:80, arg built at lines 178-180. Another simple string flag pattern.

Affected Files

Core Changes

  • src/utils/claude.ts:56-83 - ClaudeCliOptions interface - add bare?: boolean
  • src/utils/claude.ts:158-243 - launchClaude() arg building - add --bare flag support
  • src/utils/claude.ts:648-652 - generateBranchName() - add bare: true conditionally based on ANTHROPIC_API_KEY
  • src/lib/CommitManager.ts:340-347 - generateClaudeCommitMessage() claude options - add bare: true conditionally based on ANTHROPIC_API_KEY

Test Changes

  • src/utils/claude.test.ts - Tests for generateBranchName (line 2307+) and launchClaude bare flag support
  • src/lib/CommitManager.test.ts - Tests for commit message generation with/without bare mode

Architectural Flow Analysis

Data Flow: bare flag

Entry Point: src/utils/claude.ts:648 and src/lib/CommitManager.ts:340 - decided locally at call sites based on process.env.ANTHROPIC_API_KEY

Flow Path:

  1. src/utils/claude.ts:648 / src/lib/CommitManager.ts:355 - Call site sets bare: true in ClaudeCliOptions if ANTHROPIC_API_KEY is present
  2. src/utils/claude.ts:155 - launchClaude() destructures bare from options
  3. src/utils/claude.ts:~175 (new code) - arg builder pushes --bare to args array
  4. src/utils/claude.ts:264/295 - execa('claude', args, ...) passes --bare to Claude CLI process

Affected Interfaces (ALL must be updated):

  • ClaudeCliOptions at src/utils/claude.ts:56-83 - Add bare?: boolean

Critical Implementation Note: This is a simple pass-through. The bare flag only affects ClaudeCliOptions and the arg-building in launchClaude(). No other interfaces in the chain need modification because the decision is made locally at each call site.

Integration Points

  • launchClaude() is the single bottleneck for all Claude CLI invocations. Adding --bare support here automatically makes it available to all callers.
  • The env option on ClaudeCliOptions already exists for passing extra env vars - this is NOT the right approach since --bare is a CLI flag, not an env var.

Medium Severity Risks

  • Future callers may pass bare: true without understanding the auth implications: Adding a code comment near the bare option in ClaudeCliOptions documenting the auth restriction would help.

@acreeger
Copy link
Copy Markdown
Collaborator Author

acreeger commented Mar 30, 2026

Implementation Plan for Issue #977

Summary

Add --bare flag support to Claude CLI invocations used for branch naming (generateBranchName) and commit message generation (generateClaudeCommitMessage). The --bare flag reduces startup overhead by skipping hooks, LSP, plugins, and CLAUDE.md auto-discovery. Since --bare disables OAuth/keychain auth (it sets CLAUDE_CODE_SIMPLE=1), the flag is only used when ANTHROPIC_API_KEY is present in the environment; otherwise, the invocation falls back to normal (non-bare) mode.

Questions and Key Decisions

Question Answer Rationale
Should --bare be unconditionally used or gated by env var? Gated by ANTHROPIC_API_KEY presence --bare sets CLAUDE_CODE_SIMPLE=1 which broke OAuth auth in commit 308e55c. Users without ANTHROPIC_API_KEY (majority using OAuth) would get auth errors.
Should a shared helper be created for the env check? Yes, shouldUseBareMode() in claude.ts Both call sites need the same check. DRY principle avoids duplicating the env var check and keeps fallback behavior consistent.
Should --bare require headless mode (like --no-session-persistence)? No, add unconditionally when bare: true --bare is a global mode flag, not print-mode-specific. Both current call sites are headless, but the option should be general.
Does --bare conflict with --add-dir? No Per docs, --add-dir is explicitly listed as compatible with --bare for providing context.
Should --settings with apiKeyHelper be attempted for OAuth users? No (first pass) Issue says "if it fails, just fall back to non --bare mode." The simpler env var check is preferable as a first pass.

High-Level Execution Phases

  1. Interface + Arg Builder + Helper: Add bare option to ClaudeCliOptions, add arg-building logic in launchClaude(), create shouldUseBareMode() helper
  2. Call Sites: Pass bare: shouldUseBareMode() from both generateBranchName() and generateClaudeCommitMessage()
  3. Tests: Add tests for --bare flag in launchClaude(), update existing tests for both call sites, add shouldUseBareMode() tests
  4. Build: Run pnpm build to verify compilation

Quick Stats

  • 0 files for deletion
  • 2 files to modify (src/utils/claude.ts, src/lib/CommitManager.ts)
  • 0 new files to create
  • 2 test files to modify (src/utils/claude.test.ts, src/lib/CommitManager.test.ts)
  • Dependencies: None
  • Estimated complexity: Simple

Potential Risks

None beyond what was already identified in the analysis (OAuth auth breakage is mitigated by the env var check gating).


Complete Implementation Guide (click to expand for step-by-step details)

Automated Test Cases to Create

Test File: src/utils/claude.test.ts (MODIFY)

Purpose: Test --bare flag handling in launchClaude(), shouldUseBareMode() helper, and bare mode in generateBranchName()

Click to expand test structure (30 lines)
describe('bare parameter', () => {
  it('should add --bare flag when bare is true')
  // Call launchClaude with { headless: true, bare: true }
  // Assert args contain '--bare'

  it('should not add --bare flag when bare is false')
  // Call launchClaude with { headless: true, bare: false }
  // Assert args do not contain '--bare'

  it('should not add --bare flag when bare is undefined')
  // Call launchClaude with { headless: true }
  // Assert args do not contain '--bare'

  it('should combine --bare with other options in correct order')
  // Call launchClaude with { headless: true, model: 'opus', addDir: '/workspace', bare: true, noSessionPersistence: true }
  // Assert args contain '--bare' before '--model' and other options
})

describe('shouldUseBareMode', () => {
  it('should return true when ANTHROPIC_API_KEY is set')
  // Set process.env.ANTHROPIC_API_KEY, call shouldUseBareMode(), assert true

  it('should return false when ANTHROPIC_API_KEY is not set')
  // Delete process.env.ANTHROPIC_API_KEY, call shouldUseBareMode(), assert false

  it('should return false when ANTHROPIC_API_KEY is empty string')
  // Set process.env.ANTHROPIC_API_KEY = '', call shouldUseBareMode(), assert false
})

describe('generateBranchName - bare mode', () => {
  it('should pass bare: true when ANTHROPIC_API_KEY is set')
  // Set env, mock Claude CLI, call generateBranchName
  // Assert launchClaude called with { bare: true, ... }

  it('should pass bare: false when ANTHROPIC_API_KEY is not set')
  // Unset env, mock Claude CLI, call generateBranchName
  // Assert launchClaude called with { bare: false, ... } or bare absent
})

Test File: src/lib/CommitManager.test.ts (MODIFY)

Purpose: Test bare mode integration in commit message generation

Click to expand test structure (15 lines)
describe('Claude Integration - Bare Mode', () => {
  it('should pass bare: true to Claude when ANTHROPIC_API_KEY is set')
  // Set process.env.ANTHROPIC_API_KEY, trigger commit
  // Assert claude.launchClaude called with options containing { bare: true }

  it('should not pass bare: true when ANTHROPIC_API_KEY is not set')
  // Unset process.env.ANTHROPIC_API_KEY, trigger commit
  // Assert claude.launchClaude called with options NOT containing bare: true
})

Files to Modify

1. src/utils/claude.ts:56-83 — ClaudeCliOptions interface

Change: Add bare?: boolean property to the interface

At line 83 (before closing brace), add:

bare?: boolean // Minimal mode: skip hooks, LSP, plugins, CLAUDE.md auto-discovery. Requires ANTHROPIC_API_KEY (disables OAuth/keychain).

2. src/utils/claude.ts:155 — launchClaude() destructuring

Change: Add bare to the destructured options

Add bare to the destructuring on line 155, alongside existing options.

3. src/utils/claude.ts:172-173 — launchClaude() arg builder (insert after headless block)

Change: Add --bare flag to args when bare option is true

Insert after line 172 (end of headless block):

if (bare) {
  args.push('--bare')
}

4. src/utils/claude.ts — New exported helper function (insert before generateBranchName at ~line 609)

Change: Create shouldUseBareMode() helper

// Check if --bare mode can be used (requires ANTHROPIC_API_KEY, since --bare disables OAuth/keychain)
export function shouldUseBareMode(): boolean {
  return !!process.env.ANTHROPIC_API_KEY
}

5. src/utils/claude.ts:648-652 — generateBranchName() launchClaude call

Change: Add bare: shouldUseBareMode() to the options object

Change the launchClaude call options from:

{ model, headless: true, noSessionPersistence: true }

to:

{ model, headless: true, noSessionPersistence: true, bare: shouldUseBareMode() }

Also add a debug log before the call:

logger.debug('Bare mode for branch name generation', { bare: shouldUseBareMode() })

6. src/lib/CommitManager.ts:1 — Add import

Change: Import shouldUseBareMode from ../utils/claude.js

Add shouldUseBareMode to the existing imports from ../utils/claude.js (the file already imports detectClaudeCli and launchClaude).

7. src/lib/CommitManager.ts:340-347 — generateClaudeCommitMessage() claude options

Change: Add bare: shouldUseBareMode() to the options object

Change the claudeOptions from:

const claudeOptions = {
  headless: true,
  addDir: worktreePath,
  model: 'claude-haiku-4-5-20251001',
  timeout: 120000,
  appendSystemPrompt: '...',
  noSessionPersistence: true,
}

to include bare: shouldUseBareMode():

const claudeOptions = {
  headless: true,
  addDir: worktreePath,
  model: 'claude-haiku-4-5-20251001',
  timeout: 120000,
  appendSystemPrompt: '...',
  noSessionPersistence: true,
  bare: shouldUseBareMode(),
}

8. src/utils/claude.test.ts — Add tests for bare flag handling

Change: Add test describe blocks for bare parameter in launchClaude(), shouldUseBareMode(), and bare mode in generateBranchName()

  • Add describe('bare parameter', ...) block after the existing noSessionPersistence parameter describe block (after line 2121)
  • Add describe('shouldUseBareMode', ...) block
  • Add bare mode test to generateBranchName describe block
  • Import shouldUseBareMode at line 5

9. src/lib/CommitManager.test.ts — Add bare mode tests

Change: Add test describe block for bare mode in commit message generation

  • Add describe('Claude Integration - Bare Mode', ...) block after the existing Claude Integration - Fallback Behavior describe block (after line 729)
  • Tests will mock claude.shouldUseBareMode to control behavior
  • Add shouldUseBareMode to the claude mock usage at top of file

10. src/utils/claude.test.ts:2329 — Update existing generateBranchName test assertion

Change: Update the expected args array to include --bare when ANTHROPIC_API_KEY is set

The existing test at line 2327-2334 expects specific args. Since the test runs without ANTHROPIC_API_KEY set, --bare should NOT appear in the expected args. No change needed here unless tests set the env var. Verify by examining the test environment setup.

Note: The existing test at line 2329 checks exact args ['-p', '--output-format', 'stream-json', '--verbose', '--model', 'haiku', '--add-dir', '/tmp', '--no-session-persistence']. Since shouldUseBareMode() checks process.env.ANTHROPIC_API_KEY, and the test doesn't set this env var, the args will remain unchanged for existing tests. New tests will explicitly set the env var to test the --bare path.

Detailed Execution Order

Phase 1 (parallel): Core Implementation

Step 1a: Interface, arg builder, and helper in claude.ts

Files: src/utils/claude.ts
Contract: ClaudeCliOptions.bare?: boolean, shouldUseBareMode(): boolean

  1. Add bare?: boolean to ClaudeCliOptions at line 83 -> Verify: interface compiles
  2. Add bare to destructuring at line 155 -> Verify: no TS errors
  3. Add --bare arg push after line 172 -> Verify: if (bare) { args.push('--bare') }
  4. Add shouldUseBareMode() exported function before line 609 -> Verify: function exported
  5. Update generateBranchName() call at line 648-652 to pass bare: shouldUseBareMode() -> Verify: option included
  6. Add debug log before the launchClaude call in generateBranchName() -> Verify: debug message present

Step 1b: CommitManager integration

Files: src/lib/CommitManager.ts
Contract: Uses shouldUseBareMode() from src/utils/claude.ts

  1. Add shouldUseBareMode to import from ../utils/claude.js -> Verify: import resolves
  2. Add bare: shouldUseBareMode() to claudeOptions at line 340-347 -> Verify: option included

Phase 2 (parallel): Tests

Step 2a: claude.test.ts tests

Files: src/utils/claude.test.ts

  1. Import shouldUseBareMode at line 5 -> Verify: import added
  2. Add bare parameter describe block after line 2121 with 4 tests -> Verify: tests pass
  3. Add shouldUseBareMode describe block with 3 tests -> Verify: tests pass
  4. Add bare mode tests to generateBranchName describe block -> Verify: tests pass

Step 2b: CommitManager.test.ts tests

Files: src/lib/CommitManager.test.ts

  1. Add Claude Integration - Bare Mode describe block after line 729 -> Verify: tests pass
  2. Tests mock claude.shouldUseBareMode and check launchClaude options -> Verify: bare flag correctly passed

Phase 3 (sequential): Build Verification

Step 3: Build

Files: None (build step)

  1. Run pnpm build -> Verify: no compilation errors
  2. Run pnpm test -> Verify: all tests pass

Execution Plan

  1. Run Steps 1a, 1b in parallel (contract: shouldUseBareMode(): boolean exported from src/utils/claude.ts, ClaudeCliOptions.bare?: boolean)
  2. Run Steps 2a, 2b in parallel (tests for both files, depends on Phase 1)
  3. Run Step 3 (sequential build verification)

Dependencies and Configuration

None

@acreeger
Copy link
Copy Markdown
Collaborator Author

acreeger commented Mar 30, 2026

Implementation Complete

Summary

Added --bare flag support to Claude CLI invocations for branch name generation and commit message generation. When ANTHROPIC_API_KEY is available, --bare mode is used to skip hooks, LSP, plugins, and CLAUDE.md auto-discovery for faster startup. When the key is absent, the flag is omitted to preserve OAuth/keychain auth.

Changes Made

  • src/utils/claude.ts: Added bare?: boolean to ClaudeCliOptions, --bare arg handling in launchClaude(), exported shouldUseBareMode() helper, wired into generateBranchName()
  • src/lib/CommitManager.ts: Imported shouldUseBareMode and added bare: shouldUseBareMode() to generateClaudeCommitMessage()
  • src/utils/claude.test.ts: Added 9 tests for --bare param, shouldUseBareMode(), and bare mode in generateBranchName()
  • src/lib/CommitManager.test.ts: Added 2 tests for bare mode in commit message generation

Validation Results

  • ✅ Tests: 5057 passed / 1 skipped
  • ✅ Build: Passed

Detailed Changes by File (click to expand)

src/utils/claude.ts

Changes: Core --bare flag infrastructure

  • Added bare?: boolean to ClaudeCliOptions interface
  • Added --bare arg push in launchClaude() when bare is true
  • Created shouldUseBareMode(): boolean - returns true when ANTHROPIC_API_KEY is defined
  • Updated generateBranchName() to pass bare: shouldUseBareMode()

src/lib/CommitManager.ts

Changes: Bare mode integration for commit messages

  • Imported shouldUseBareMode from ../utils/claude.js
  • Added bare: shouldUseBareMode() to claude options in generateClaudeCommitMessage()

src/utils/claude.test.ts

Changes: Test coverage for bare flag

  • 4 tests for --bare parameter in launchClaude()
  • 3 tests for shouldUseBareMode() helper
  • 2 tests for bare mode in generateBranchName()

src/lib/CommitManager.test.ts

Changes: Test coverage for CommitManager bare mode

  • 2 tests verifying bare mode passed to launchClaude options

Auto-apply --bare mode in launchClaude() for headless utility operations
(branch naming, commit messages) when ANTHROPIC_API_KEY is available.
This skips hooks, LSP, plugins, and CLAUDE.md auto-discovery for faster
startup. Falls back to normal mode when only OAuth/keychain auth is
available, avoiding the regression from 308e55c.

Fixes #977
@acreeger acreeger force-pushed the feat/issue-977__bare-flag branch from 603e7e5 to 61c9740 Compare March 30, 2026 08:56
@acreeger
Copy link
Copy Markdown
Collaborator Author

Analysis: --bare flag with OAuth via apiKeyHelper

  • Fetch issue details and understand the problem
  • Research commit 308e55c (CLAUDE_CODE_SIMPLE removal)
  • Investigate Claude Code --bare mode and --settings / apiKeyHelper
  • Trace current OAuth token storage in ~/.claude/
  • Map how iloom currently invokes Claude CLI (branch naming, commit messages)
  • Analyze architectural flow for --bare + apiKeyHelper integration
  • Document findings

Status: Starting research...

@acreeger
Copy link
Copy Markdown
Collaborator Author

acreeger commented Mar 30, 2026

Analysis: --bare flag with OAuth via apiKeyHelper

  • Fetch issue details and understand requirements
  • Research commit 308e55c (CLAUDE_CODE_SIMPLE removal)
  • Investigate Claude Code --bare mode and --settings/apiKeyHelper behavior
  • Trace current OAuth token storage and retrieval
  • Map current Claude CLI invocation paths in iloom codebase
  • Analyze feasibility of --bare + OAuth via apiKeyHelper
  • Document findings

Executive Summary

The --bare flag provides significant startup performance gains for headless utility calls (branch naming, commit messages) by skipping hooks, LSP, plugins, and CLAUDE.md discovery. Currently it only activates when ANTHROPIC_API_KEY is set, meaning OAuth-only users (the majority) never benefit. The issue asks us to also enable --bare for OAuth users by extracting the cached OAuth token and passing it to Claude Code via --settings '{"apiKeyHelper": "..."}'. This is feasible: on macOS, the token lives in Keychain; on Linux, in ~/.claude/.credentials.json. The approach needs a fallback to non-bare mode when token extraction fails.

Question Answer
Should apiKeyHelper be a shell script on disk or can we use an inline command? --settings accepts inline JSON: --settings '{"apiKeyHelper": "echo TOKEN"}'. Inline is simpler - no temp file needed. The value is a shell command whose stdout is used as the API key.
Does Claude Code accept OAuth tokens (sk-ant-oat01-...) via apiKeyHelper? Yes - the docs state apiKeyHelper output is used for X-Api-Key and Authorization: Bearer headers. OAuth tokens with the sk-ant-oat01- prefix work as API keys for the Anthropic API.
Should we attempt token refresh if expired? No. If the token is expired, the extraction will still return it, but the API call will fail. The correct behavior is to fall back to non-bare mode, where Claude Code handles its own OAuth flow including refresh.
Should the keychain-reading logic live in shouldUseBareMode or separately? Separately. shouldUseBareMode should remain a fast synchronous check. A new async function (e.g., getBareModeSettings) should handle token extraction and settings JSON generation.

HIGH/CRITICAL Risks

  • Token expiry causes silent failures: If the extracted OAuth token has expired, --bare will fail with an auth error. The implementation MUST catch this and fall back to non-bare mode, otherwise branch naming and commit message generation will fail entirely.
  • Keychain access may be blocked: On macOS, reading the keychain via security find-generic-password can fail silently (locked keychain in SSH sessions, no GUI available). This is analogous to the exact issue that caused commit 308e55c - CLAUDE_CODE_SIMPLE broke OAuth because it skipped keychain reads. The apiKeyHelper approach must handle this gracefully.

Impact Summary

  • 1 primary file to modify: src/utils/claude.ts (shouldUseBareMode, launchClaude, new token extraction function)
  • 1 interface to update: ClaudeCliOptions in src/utils/claude.ts (add settings field)
  • 1 test file: src/utils/claude.test.ts (shouldUseBareMode tests, new bare+OAuth tests)
  • 8 call sites auto-benefit (no changes needed): all callers using headless: true + noSessionPersistence: true
  • Cross-platform credential extraction needed: macOS (Keychain) + Linux (.credentials.json)

Complete Technical Reference (click to expand for implementation details)

Problem Space Research

Problem Understanding

The --bare flag makes headless Claude CLI calls faster by skipping startup overhead (hooks, LSP, plugins, CLAUDE.md walks). These utility calls (branch naming, commit messages, PR bodies) are performance-sensitive since users wait for them inline. Currently, only API key users get this benefit. Since most users authenticate via OAuth (Claude Max subscription), the feature is effectively unused.

Architectural Context

The shouldUseBareMode() function at src/utils/claude.ts:622 is the single gate. It's called from launchClaude() at line 163 as an auto-apply mechanism when headless && noSessionPersistence and bare isn't explicitly set. The --bare flag description confirms: "Anthropic auth is strictly ANTHROPIC_API_KEY or apiKeyHelper via --settings".

Historical Context: Commit 308e55c

The previous attempt to optimize utility calls used CLAUDE_CODE_SIMPLE=1 env var (which --bare now also sets internally). This was removed in commit 308e55c because CLAUDE_CODE_SIMPLE=1 disables OAuth credential lookup entirely, causing "Not logged in" errors for OAuth-only users. The key insight: --bare has the same OAuth-disabling behavior, but provides a recovery path via apiKeyHelper that CLAUDE_CODE_SIMPLE alone did not.

Edge Cases

  • Token expiry: OAuth tokens have finite lifetimes (expiresAt is a Unix ms timestamp). Claude Code refreshes tokens internally, but an extracted token is a snapshot.
  • Multiple keychain accounts: macOS keychain can have multiple Claude Code-credentials entries (observed: account=unknown for MCP OAuth, account=$USER for main credentials).
  • No credentials available: First-time users or fresh installs may have no cached OAuth token.
  • Keychain locked/inaccessible: SSH sessions, cron jobs, or headless Linux environments without keychain daemon.
  • Inline JSON escaping: The OAuth token may contain characters that need shell escaping when passed via --settings.

Third-Party Research Findings

Claude Code CLI --bare flag

Source: WebSearch + CLI help text
Key Findings:

  • --bare sets CLAUDE_CODE_SIMPLE=1 internally
  • Auth is strictly ANTHROPIC_API_KEY env var OR apiKeyHelper via --settings
  • OAuth and keychain are never read in bare mode
  • --settings <file-or-json> accepts either a file path or inline JSON string
  • apiKeyHelper value is a shell command that outputs the API key to stdout
  • apiKeyHelper is called after 5 minutes or on HTTP 401 (for refresh)

Claude Code Credential Storage

Source: WebSearch + local filesystem investigation
Key Findings:

  • macOS: Stored in macOS Keychain, service=Claude Code-credentials, account=$USER
    • Readable via: security find-generic-password -s "Claude Code-credentials" -a "$USER" -w
    • Returns JSON: {"claudeAiOauth":{"accessToken":"sk-ant-oat01-...","refreshToken":"...","expiresAt":1774887933084,"scopes":[...],"subscriptionType":"max"},"mcpOAuth":{...}}
  • Linux: Stored in ~/.claude/.credentials.json with mode 0600
    • Same JSON structure as macOS keychain value
  • Windows: Stored in ~/.claude/.credentials.json inheriting user profile ACLs
  • Env var override: CLAUDE_CODE_OAUTH_TOKEN env var can be used as alternative

Codebase Research Findings

Affected Area: Claude CLI utility layer

Entry Point: src/utils/claude.ts:622 - shouldUseBareMode() function
Dependencies:

  • Uses: process.env.ANTHROPIC_API_KEY
  • Used By: launchClaude() at line 163 (auto-apply logic)

Entry Point: src/utils/claude.ts:152-555 - launchClaude() function
Dependencies:

  • Uses: shouldUseBareMode(), execa, ClaudeCliOptions
  • Used By: All headless utility operations across the codebase

Call Sites That Auto-Trigger Bare Mode

These call sites pass headless: true + noSessionPersistence: true, which triggers the shouldUseBareMode() check:

  1. src/utils/claude.ts:665-668 - generateBranchName() - branch name generation
  2. src/lib/CommitManager.ts:341-346 - generateClaudeCommitMessage() - commit messages
  3. src/lib/PRManager.ts:72-76 - PR body generation
  4. src/lib/IssueEnhancementService.ts:114-118 - Issue enhancement (single)
  5. src/lib/IssueEnhancementService.ts:254-258 - Issue enhancement (batch)
  6. src/lib/SessionSummaryService.ts:138-142 - Session summary generation
  7. src/lib/SessionSummaryService.ts:342-346 - Session summary (alternate path)
  8. src/lib/ValidationRunner.ts:414-419 - Validation (conditional headless)
  9. src/lib/MergeManager.ts:510-516 - Merge conflict resolution (conditional headless)

ClaudeCliOptions Interface

src/utils/claude.ts:56-84 - Currently lacks settings field. The interface needs a new optional field for --settings.

Current Bare Mode Logic

src/utils/claude.ts:162-167
const effectiveBare = bare ?? (headless && noSessionPersistence ? shouldUseBareMode() : false)
if (effectiveBare) {
    args.push('--bare')
}

The auto-apply only adds --bare to args. It does not add --settings. For OAuth token flow, when shouldUseBareMode() returns true due to OAuth token availability, the --settings JSON with apiKeyHelper must also be added.

Platform-Specific Code Patterns

The codebase already handles platform differences (e.g., src/commands/shell.ts:252 checks process.platform === 'win32'). The credential extraction will need similar branching.

Affected Files

  • src/utils/claude.ts:56-84 - ClaudeCliOptions interface - Add settings field
  • src/utils/claude.ts:152-167 - launchClaude() - Add --settings arg building + bare mode OAuth logic
  • src/utils/claude.ts:617-624 - shouldUseBareMode() - Expand to check for OAuth token availability (or create new async function)
  • src/utils/claude.test.ts:2295-2329 - Existing shouldUseBareMode tests - Update for new OAuth path
  • src/utils/claude.test.ts:2175-2258 - Existing bare mode tests - Add OAuth variant tests

Architectural Flow Analysis

Data Flow: OAuth token to --bare --settings

Entry Point: OS credential store (macOS Keychain / Linux ~/.claude/.credentials.json)

Flow Path:

  1. New function (e.g., getOAuthTokenForBareMode()) extracts token from platform-specific store
  2. shouldUseBareMode() (or new async variant) returns true when token is available
  3. launchClaude() at line 162-167 detects bare mode should be active
  4. New logic generates --settings '{"apiKeyHelper": "echo <token>"}' arg
  5. Claude CLI receives --bare --settings ... and uses apiKeyHelper for auth
  6. On auth failure (expired token, etc.), catch block at lines 441-553 handles the error

Affected Interfaces (ALL must be updated):

  • ClaudeCliOptions at src/utils/claude.ts:56-84 - Add optional settings?: string | Record<string, unknown>

Critical Implementation Note: The shouldUseBareMode() function is currently synchronous. Reading from Keychain requires spawning a subprocess (security command on macOS), which is async. Either: (a) make it async, (b) create a separate async function that the sync function delegates to at launchClaude callsite, or (c) read credentials at startup and cache.

Integration Points

  • launchClaude() is the single point where --bare and --settings args are built - no other files need modification for the core flow
  • All 9 call sites automatically benefit because they don't set bare explicitly - they rely on the auto-apply logic at line 163
  • Error handling at lines 441-553 already catches Claude CLI errors via stderr - auth failures from expired tokens will be caught here

Medium Severity Risks

  • Shell escaping of OAuth token in apiKeyHelper: The token is base64-ish and may contain characters that need escaping in echo "TOKEN" command passed inline. Using single quotes around the token in the echo command should be sufficient, but tokens with single quotes would break.
  • Performance of credential extraction: Reading from macOS Keychain (security CLI) or filesystem adds latency to every utility call. Could be mitigated by caching the token in memory for the process lifetime.
  • CLAUDE_CODE_OAUTH_TOKEN env var: If this env var is set, it could be used directly instead of reading from Keychain, providing a simpler path. Worth checking if callers might set this.

@acreeger
Copy link
Copy Markdown
Collaborator Author

acreeger commented Mar 30, 2026

Implementation Plan for Issue #977 -- OAuth via apiKeyHelper for --bare mode

Summary

Extend the existing --bare mode auto-apply logic to also work for OAuth-authenticated users. Currently, bare mode only activates when ANTHROPIC_API_KEY is set. This change adds platform-specific OAuth token extraction (macOS Keychain, Linux ~/.claude/.credentials.json) and passes the token to Claude Code via --settings '{"apiKeyHelper": "echo TOKEN"}' alongside --bare. If the OAuth token is expired or extraction fails, the system falls back to non-bare mode with a visible log warning. All 9 existing call sites that use headless: true + noSessionPersistence: true automatically benefit.

Questions and Key Decisions

Question Answer Rationale
Where should OAuth token extraction live? New async function extractOAuthToken() in claude.ts Keeps credential logic co-located with bare mode logic. Single file modification for the core flow.
Should shouldUseBareMode() become async? No -- keep it sync. Create new resolveBareModeConfig() async function shouldUseBareMode() is exported and may be used synchronously elsewhere. The new async function handles OAuth. launchClaude() is already async so awaiting is free.
Where does the retry-without-bare live? Inside launchClaude() catch block Matches existing session-ID retry pattern. All 9 call sites benefit automatically.
Should explicitly-set bare: true also retry? No -- only auto-applied bare retries If caller explicitly sets bare: true and it fails, that's their intent. Don't silently change it. Track via bareModeAutoApplied flag.
How is the OAuth token passed to Claude? --settings '{"apiKeyHelper": "echo TOKEN"}' inline JSON Analysis confirms --settings accepts inline JSON. apiKeyHelper value is a shell command whose stdout is the API key. No temp file needed.
Should CLAUDE_CODE_OAUTH_TOKEN env var be checked? Yes, as first fallback before Keychain/file It's already used in GitHub Actions (.github/workflows/claude.yml:37). Provides a simpler, faster path when available.
How to handle token escaping in echo command? Use printf '%s' TOKEN instead of echo More robust than echo for tokens that might contain special characters. Single quotes around token handle most cases.

High-Level Execution Phases

  1. New functions + interface changes: Add extractOAuthToken(), resolveBareModeConfig(), settings field to ClaudeCliOptions, and --settings arg building in launchClaude()
  2. Modify auto-apply logic: Update launchClaude() lines 162-167 to use async resolveBareModeConfig() and add retry-without-bare on auth failure
  3. Tests: Add tests for token extraction, bare mode config resolution, --settings arg, and auth failure retry
  4. Build verification: pnpm build + pnpm test

Quick Stats

  • 0 files for deletion
  • 1 file to modify: src/utils/claude.ts
  • 0 new files to create
  • 1 test file to modify: src/utils/claude.test.ts
  • Dependencies: None
  • Estimated complexity: Medium

Potential Risks (HIGH/CRITICAL only)

  • macOS Keychain access may prompt user or fail silently: In SSH sessions or locked keychain scenarios, security find-generic-password may hang or fail. Mitigated by a 3-second timeout on the execa call and fallback to non-bare mode.
  • Token expiry causes auth error on first attempt: The extracted OAuth token may be expired. Mitigated by the retry-without-bare logic in launchClaude(), which catches auth errors and re-runs without --bare/--settings.

Complete Implementation Guide (click to expand for step-by-step details)

Automated Test Cases to Create

Test File: src/utils/claude.test.ts (MODIFY)

Purpose: Test OAuth token extraction, bare mode config resolution, --settings arg handling, and auth failure retry

Click to expand complete test structure (~80 lines)
describe('extractOAuthToken', () => {
  // Save/restore process.platform and env

  it('should return CLAUDE_CODE_OAUTH_TOKEN env var if set')
  // Set env var, call extractOAuthToken(), assert returns the token

  it('should return null when CLAUDE_CODE_OAUTH_TOKEN is empty')
  // Set env var to '', assert null

  describe('macOS (darwin)', () => {
    // Mock process.platform = 'darwin'

    it('should extract token from macOS Keychain via security command')
    // Mock execa('security', [...]) to return JSON with claudeAiOauth.accessToken
    // Assert returns the token

    it('should return null when security command fails')
    // Mock execa to throw, assert returns null

    it('should return null when keychain JSON is malformed')
    // Mock execa to return invalid JSON, assert returns null

    it('should return null when claudeAiOauth.accessToken is missing')
    // Mock execa to return JSON without the token field, assert returns null

    it('should use 3-second timeout on security command')
    // Verify execa called with { timeout: 3000 }
  })

  describe('Linux/other platforms', () => {
    // Mock process.platform = 'linux'

    it('should extract token from ~/.claude/.credentials.json')
    // Mock readFile to return JSON with claudeAiOauth.accessToken
    // Assert returns the token

    it('should return null when credentials file does not exist')
    // Mock readFile to throw ENOENT, assert returns null

    it('should return null when credentials JSON is malformed')
    // Mock readFile to return invalid JSON, assert returns null
  })
})

describe('resolveBareModeConfig', () => {
  it('should return { bare: true } when ANTHROPIC_API_KEY is set')
  // No settings needed when API key is available

  it('should return { bare: true, settings: ... } when OAuth token is available')
  // No ANTHROPIC_API_KEY, but OAuth token extracted
  // Assert settings contains apiKeyHelper with the token

  it('should return { bare: false } when neither API key nor OAuth token available')
  // Assert bare is false

  it('should log debug message about bare mode source')
  // Verify logger.debug called with source info
})

describe('settings parameter', () => {
  it('should add --settings flag when settings is provided')
  // Call launchClaude with { headless: true, settings: '{"apiKeyHelper": "echo token"}' }
  // Assert args contain '--settings' followed by the JSON

  it('should not add --settings flag when settings is undefined')
  // Call launchClaude without settings, assert no --settings in args
})

describe('bare mode auth failure retry', () => {
  it('should retry without --bare when auto-applied bare mode fails with auth error')
  // First call: execa rejects with stderr containing auth error
  // Second call: execa resolves with output
  // Assert second call does NOT have --bare or --settings in args
  // Assert logger.warn called with visible fallback message

  it('should NOT retry when bare was explicitly set by caller')
  // Call with { bare: true }, execa rejects with auth error
  // Assert error is thrown, NOT retried

  it('should NOT retry for non-auth errors')
  // Auto-applied bare, execa rejects with non-auth error (e.g., timeout)
  // Assert error is thrown, NOT retried
})

Files to Modify

1. src/utils/claude.ts:1-8 -- Add imports

Change: Add readFile from node:fs/promises and homedir from node:os

import { readFile } from 'node:fs/promises'
import { homedir } from 'node:os'

2. src/utils/claude.ts:56-84 -- ClaudeCliOptions interface

Change: Add settings?: string property

At line 83 (after bare), add:

settings?: string // JSON settings string for --settings flag (e.g., '{"apiKeyHelper": "echo TOKEN"}')

3. src/utils/claude.ts (insert before shouldUseBareMode at ~line 617) -- New extractOAuthToken() function

Change: Create async function that extracts OAuth token from platform-specific credential stores

Pseudocode (~40 lines):

export async function extractOAuthToken(): Promise<string | null> {
  // 1. Check CLAUDE_CODE_OAUTH_TOKEN env var first (fastest path)
  // 2. Platform branch:
  //    - macOS: execa('security', ['find-generic-password', '-s', 'Claude Code-credentials', '-a', username, '-w'], { timeout: 3000 })
  //      Parse JSON response, extract claudeAiOauth.accessToken
  //    - Linux/other: readFile(join(homedir(), '.claude', '.credentials.json'), 'utf-8')
  //      Parse JSON, extract claudeAiOauth.accessToken
  // 3. All errors caught -> logger.debug() -> return null
}

4. src/utils/claude.ts (insert after extractOAuthToken) -- New resolveBareModeConfig() function

Change: Create async function that determines bare mode + settings

Pseudocode (~20 lines):

export async function resolveBareModeConfig(): Promise<{ bare: boolean; settings?: string }> {
  // 1. If ANTHROPIC_API_KEY is set: return { bare: true } (existing fast path)
  // 2. Try extractOAuthToken()
  //    - If token found: return { bare: true, settings: JSON.stringify({ apiKeyHelper: `printf '%s' '${token}'` }) }
  //    - Log debug: "Using OAuth token for bare mode via apiKeyHelper"
  // 3. If no token: return { bare: false }
  //    - Log debug: "No API key or OAuth token available, bare mode disabled"
}

5. src/utils/claude.ts:156 -- launchClaude() destructuring

Change: Add settings to destructured options

Add settings to the destructuring alongside bare.

6. src/utils/claude.ts:162-167 -- launchClaude() auto-apply bare mode logic

Change: Replace sync shouldUseBareMode() with async resolveBareModeConfig() and track whether bare was auto-applied

Replace:

const effectiveBare = bare ?? (headless && noSessionPersistence ? shouldUseBareMode() : false)

if (effectiveBare) {
    args.push('--bare')
}

With (pseudocode):

let effectiveBare = bare ?? false
let effectiveSettings = settings
let bareModeAutoApplied = false

if (bare === undefined && headless && noSessionPersistence) {
    const config = await resolveBareModeConfig()
    effectiveBare = config.bare
    effectiveSettings = config.settings
    bareModeAutoApplied = config.bare  // track that WE decided to use bare
}

if (effectiveBare) {
    args.push('--bare')
}

7. src/utils/claude.ts (after --bare push, before headless block at line 169) -- Add --settings arg building

Change: Push --settings when settings string is provided

if (effectiveSettings) {
    args.push('--settings', effectiveSettings)
}

8. src/utils/claude.ts:441-553 -- launchClaude() catch block -- Add auth failure retry

Change: Before the existing session-ID retry check, add an auth failure retry for auto-applied bare mode

Insert at ~line 455 (after errorMessage is built, before session-ID retry):

Pseudocode (~35 lines):

// Check for auth failure when bare mode was auto-applied (OAuth token may be expired)
const isAuthError = /not logged in|unauthorized|authentication|Invalid API Key/i.test(errorMessage)
if (isAuthError && bareModeAutoApplied && headless) {
    logger.warn('Bare mode failed (likely expired OAuth token), retrying without --bare')

    // Rebuild args without --bare and --settings
    const retryArgs = args.filter((arg, idx) => {
        if (arg === '--bare') return false
        if (arg === '--settings') return false
        if (idx > 0 && args[idx - 1] === '--settings') return false
        return true
    })

    // Re-run the headless execution path (copy the existing headless execa logic)
    // This mirrors the session-ID retry pattern already in launchClaude()
    const execaOptions = { input: prompt, timeout: 0, ...(addDir && { cwd: addDir }), ... }
    const subprocess = execa('claude', retryArgs, execaOptions)
    // ... stream handling (same as primary path) ...
    // Return result
}

Note: To avoid duplicating the headless execution logic (stream handling, progress dots, etc.), consider extracting a shared helper executeHeadlessClaude(args, ...). However, the existing session-ID retry already duplicates this logic (~45 lines at lines 470-532). For consistency with the existing pattern, the auth retry can follow the same approach. A refactor to extract the shared helper would be a nice follow-up but is out of scope for this issue.

9. src/utils/claude.test.ts -- Add new test blocks

Change: Add test blocks for extractOAuthToken, resolveBareModeConfig, settings parameter, and auth failure retry

The tests need to:

  • Mock execa for the security command (macOS Keychain)
  • Mock readFile for Linux credentials file (requires adding readFile to mock setup)
  • Mock process.platform for platform-specific tests
  • Mock process.env.CLAUDE_CODE_OAUTH_TOKEN and process.env.ANTHROPIC_API_KEY

Important: The existing test file already mocks execa globally. The new extractOAuthToken macOS tests will need to distinguish between security and claude execa calls. Use vi.mocked(execa).mockImplementation() with arg-based routing, or use separate mock calls in sequence.

The readFile mock needs to be added to the existing vi.mock('node:fs') block or a new vi.mock('node:fs/promises') block.

10. src/utils/claude.test.ts -- Update existing shouldUseBareMode tests

Change: Keep existing tests as-is (they still test the sync helper). Add a note that resolveBareModeConfig is the async alternative used by launchClaude.

No code changes needed to existing tests.

Detailed Execution Order

Phase 1 (sequential): Core implementation in src/utils/claude.ts

All changes are in a single file, so they must be sequential.

Step 1: Add imports, interface, and new functions

Files: src/utils/claude.ts

  1. Add readFile and homedir imports at lines 1-8 -> Verify: no TS errors
  2. Add settings?: string to ClaudeCliOptions at line 83 -> Verify: interface compiles
  3. Add extractOAuthToken() function before shouldUseBareMode() at ~line 617 -> Verify: function compiles
  4. Add resolveBareModeConfig() function after extractOAuthToken() -> Verify: function compiles
  5. Add settings to destructuring at line 156 -> Verify: no TS errors
  6. Replace bare mode auto-apply logic at lines 162-167 with async resolveBareModeConfig() call -> Verify: effectiveBare and bareModeAutoApplied correctly set
  7. Add --settings arg push after --bare push -> Verify: --settings appears in args when settings is provided
  8. Add auth failure retry logic in catch block at ~line 455 -> Verify: retry executes when auth error + bareModeAutoApplied

Phase 2 (sequential): Tests in src/utils/claude.test.ts

Tests depend on the implementation being complete.

Step 2: Add tests

Files: src/utils/claude.test.ts

  1. Add readFile mock to test setup (add vi.mock('node:fs/promises')) -> Verify: mock works
  2. Add extractOAuthToken describe block with platform-specific tests -> Verify: tests pass
  3. Add resolveBareModeConfig describe block -> Verify: tests pass
  4. Add settings parameter describe block -> Verify: tests pass
  5. Add auth failure retry describe block -> Verify: tests pass
  6. Update existing shouldUseBareMode tests if needed (likely no changes) -> Verify: existing tests still pass

Phase 3 (sequential): Build verification

Step 3: Build and test

Files: None (build step)

  1. Run pnpm build -> Verify: no compilation errors
  2. Run pnpm test -> Verify: all tests pass

Execution Plan

  1. Run Step 1 (sequential - all changes in src/utils/claude.ts)
  2. Run Step 2 (sequential - depends on Step 1, all changes in src/utils/claude.test.ts)
  3. Run Step 3 (sequential - build verification)

Note: Steps cannot be parallelized because Step 2 imports functions created in Step 1, and both steps modify single files. Step 3 validates everything.

Dependencies and Configuration

None

@acreeger
Copy link
Copy Markdown
Collaborator Author

acreeger commented Mar 30, 2026

Implementation Complete - Issue #977

Summary

Extended bare mode auto-apply logic to work for OAuth-authenticated users. When ANTHROPIC_API_KEY is not set, the system now extracts OAuth tokens from platform-specific credential stores (macOS Keychain or ~/.claude/.credentials.json) and passes them to Claude Code via --settings '{"apiKeyHelper": "printf TOKEN"}'. If bare mode fails due to auth errors (e.g., expired token), it automatically retries without --bare.

Changes Made

  • src/utils/claude.ts: Added extractOAuthToken() (platform-specific OAuth token extraction), resolveBareModeConfig() (async bare mode resolution with OAuth support), settings field on ClaudeCliOptions, --settings arg building, and auth failure retry with fallback for auto-applied bare mode
  • src/utils/claude.test.ts: Added tests for extractOAuthToken (env var, macOS keychain, Linux file, expired token, errors), resolveBareModeConfig (API key path, OAuth path, no auth), --settings parameter handling, and auth failure retry (auto-applied retry, explicit no-retry, non-auth no-retry). Updated existing tests to account for OAuth extraction in the mock chain.

Validation Results

  • Tests: 5080 passed / 5081 total (1 skipped - platform-conditional)
  • Typecheck: Passed
  • Lint: Passed
  • Build: Passed

Detailed Changes by File (click to expand)

Files Modified

src/utils/claude.ts

Changes: Added OAuth token extraction and bare mode configuration resolution

  • Added imports: readFile from node:fs/promises, homedir from node:os
  • Added settings?: string field to ClaudeCliOptions interface
  • Added extractOAuthToken(): Checks CLAUDE_CODE_OAUTH_TOKEN env var first, then macOS Keychain (security find-generic-password with 3s timeout), then Linux ~/.claude/.credentials.json. Validates token expiry. Returns null on any error.
  • Added resolveBareModeConfig(): Returns { bare: true } for API key, { bare: true, settings: ... } for OAuth token with apiKeyHelper using printf '%s', or { bare: false } when neither available.
  • Updated launchClaude() destructuring to include settings
  • Replaced sync shouldUseBareMode() call with async resolveBareModeConfig() in auto-apply block, tracking bareModeAutoApplied flag
  • Added --settings arg push when effectiveSettings is provided
  • Added auth failure retry in catch block: detects auth errors via regex, only retries when bare was auto-applied (not explicitly set), logs warning, rebuilds args without --bare/--settings
  • Kept shouldUseBareMode() exported for backwards compatibility with updated JSDoc

src/utils/claude.test.ts

Changes: Added comprehensive tests for new OAuth functionality and updated existing tests

  • Added readFile import and vi.mock('node:fs/promises')
  • Added extractOAuthToken and resolveBareModeConfig to imports
  • Added extractOAuthToken describe block (8 tests): env var, empty env var, macOS keychain success/failure/malformed/missing token/expired token/timeout verification, Linux file read success/failure/malformed
  • Added resolveBareModeConfig describe block (3 tests): API key path, OAuth token path, no auth available
  • Added settings parameter describe block (2 tests): --settings flag presence/absence
  • Added bare mode auth failure retry describe block (3 tests): auto-applied retry success, explicit no-retry, non-auth no-retry
  • Updated existing noSessionPersistence tests to mock OAuth extraction failure (now needed since resolveBareModeConfig is called)
  • Updated generateBranchName tests with mockOAuthExtractionFailure() helper and env var management

Dependencies Added

None

@acreeger
Copy link
Copy Markdown
Collaborator Author

acreeger commented Apr 1, 2026

Combined Analysis & Plan - Issue #977

Status: In Progress

  • Fetch issue and understand requirements
  • Research codebase (SessionSummaryService, launchClaude, ClaudeModel type)
  • Formulate implementation plan
  • Write up final analysis and plan

@acreeger
Copy link
Copy Markdown
Collaborator Author

acreeger commented Apr 1, 2026

Combined Analysis & Plan - Issue #977 (Prompt Too Long Retry)

  • Perform lightweight analysis of SessionSummaryService
  • Create implementation plan with retry logic
  • Document combined results

Executive Summary

When il summary resumes a long Claude session, the session transcript can exceed the 200K token context window for Sonnet, causing a "Prompt is too long" error. The fix adds retry logic in SessionSummaryService that detects this error and retries the launchClaude call with opus[1m] (1M context window). A shared private helper method avoids duplicating retry logic across both session summary call sites.

Implementation Overview

High-Level Execution Phases

  1. Add private helper: Extract launchSessionSummary() in SessionSummaryService that wraps launchClaude with "Prompt is too long" retry logic
  2. Refactor call sites: Replace both inline launchClaude calls (lines 138 and 342) to use the new helper
  3. Add tests: Test retry behavior for both thrown errors and result-string errors
  4. Build: Run pnpm build to verify

Quick Stats

  • 1 file to modify: src/lib/SessionSummaryService.ts
  • 1 file to modify: src/lib/SessionSummaryService.test.ts
  • 0 new files
  • Dependencies: None

Complete Analysis & Implementation Details (click to expand)

Research Findings

Problem Space

  • Problem: launchClaude with --resume (sessionId) loads the full conversation transcript; long sessions exceed Sonnet's 200K context window, returning "Prompt is too long"
  • Architectural context: SessionSummaryService has 3 launchClaude calls; only the 2 that use sessionId are affected (lines 138 and 342)
  • Edge cases: The error can come as a thrown exception (non-zero exit) or as a returned result string (zero exit)

Codebase Research

  • Entry point: src/lib/SessionSummaryService.ts:138 and :342 -- both pass sessionId to resume the session
  • Epic report at line 247: Does NOT use sessionId -- not affected
  • Model shorthands: VALID_CLAUDE_MODELS at src/lib/SettingsManager.ts:21 includes 'opus[1m]' for 1M context
  • Summary default model: SummarySettingsSchema at src/lib/SettingsManager.ts:128-133 defaults to 'sonnet'
  • Existing retry pattern: src/utils/claude.ts:400-454 has a retry loop for bare-mode auth failures -- logger.warn() used for visible retry messages
  • Similar DRY opportunity: Both call sites at lines 138 and 342 have identical launchClaude options (headless, model, sessionId, noSessionPersistence) -- extract to shared helper

Affected Files

  • src/lib/SessionSummaryService.ts:138-143 -- generateAndPostSummary launchClaude call
  • src/lib/SessionSummaryService.ts:342-347 -- generateSummary launchClaude call
  • src/lib/SessionSummaryService.test.ts -- Test file for the service

Integration Points

  • launchClaude from src/utils/claude.ts -- already mocked in test file
  • logger from src/utils/logger.js -- used for logger.warn() visible messages

Implementation Plan

Automated Test Cases to Create

Test File: src/lib/SessionSummaryService.test.ts (MODIFY)

Click to expand complete test structure (30 lines)
describe('prompt too long retry with opus[1m]', () => {
  // Test in generateAndPostSummary context
  it('should retry with opus[1m] when launchClaude throws "Prompt is too long" error', async () => {
    // First call rejects with "Prompt is too long", second call succeeds with valid summary
    // Verify launchClaude called twice: first with 'sonnet', second with 'opus[1m]'
    // Verify comment was posted successfully
  })

  it('should retry with opus[1m] when launchClaude returns "Prompt is too long" as result text', async () => {
    // First call resolves with string containing "Prompt is too long", second call succeeds
    // Verify launchClaude called twice: first with 'sonnet', second with 'opus[1m]'
  })

  it('should not retry when launchClaude fails with a different error', async () => {
    // Rejects with "Claude API error" -- should NOT retry, should be non-blocking
    // Verify launchClaude called only once
  })

  // Test in generateSummary context
  it('should retry with opus[1m] in generateSummary when prompt is too long', async () => {
    // First call rejects with "Prompt is too long", second succeeds
    // Verify launchClaude called twice
    // Verify returned summary is from the retry call
  })

  it('should throw when retry also fails in generateSummary', async () => {
    // Both calls reject -- should throw (generateSummary is NOT non-blocking)
    // Verify launchClaude called twice
  })
})

Files to Modify

1. src/lib/SessionSummaryService.ts

Change 1: Add private helper method (insert after line ~78, after constructor)

Purpose: DRY helper that wraps launchClaude with "Prompt is too long" detection and retry using opus[1m].

// Private helper: launches Claude for session summary, retries with opus[1m] if prompt too long
private async launchSessionSummary(prompt: string, model: string, sessionId: string): Promise<string | void> {
  // Try with the configured model first
  // If result or error contains "Prompt is too long":
  //   logger.warn(`Session too long for ${model}, retrying with Opus (1M context)...`)
  //   Retry launchClaude with model: 'opus[1m]', same sessionId/headless/noSessionPersistence
  // Return the result from whichever call succeeds
}

Detection logic must handle both cases:

  • Thrown error: catch, check error.message for "Prompt is too long", if match retry, else re-throw
  • Result string: check if result string contains "Prompt is too long", if match retry

Change 2: generateAndPostSummary at lines 137-143

Replace:

const summaryModel = this.settingsManager.getSummaryModel(settings)
const summaryResult = await launchClaude(prompt, {
  headless: true,
  model: summaryModel,
  sessionId: sessionId,
  noSessionPersistence: true,
})

With:

const summaryModel = this.settingsManager.getSummaryModel(settings)
const summaryResult = await this.launchSessionSummary(prompt, summaryModel, sessionId)

Change 3: generateSummary at lines 341-347

Replace:

const summaryModel = this.settingsManager.getSummaryModel(settings)
const summaryResult = await launchClaude(prompt, {
  headless: true,
  model: summaryModel,
  sessionId: sessionId,
  noSessionPersistence: true,
})

With:

const summaryModel = this.settingsManager.getSummaryModel(settings)
const summaryResult = await this.launchSessionSummary(prompt, summaryModel, sessionId)

2. src/lib/SessionSummaryService.test.ts

Change: Add new describe('prompt too long retry') block with test cases as specified above.

Detailed Execution Order

NOTE: These steps are executed in a SINGLE implementation run.

  1. Add helper method to SessionSummaryService

    • File: src/lib/SessionSummaryService.ts
    • Add private async launchSessionSummary(prompt, model, sessionId) after the constructor (line ~78)
    • Helper tries launchClaude with given model, catches/detects "Prompt is too long", retries with opus[1m]
    • Verify: Method exists and compiles
  2. Refactor generateAndPostSummary call site

    • File: src/lib/SessionSummaryService.ts:137-143
    • Replace inline launchClaude with this.launchSessionSummary(prompt, summaryModel, sessionId)
    • Verify: Existing tests still pass
  3. Refactor generateSummary call site

    • File: src/lib/SessionSummaryService.ts:341-347
    • Replace inline launchClaude with this.launchSessionSummary(prompt, summaryModel, sessionId)
    • Verify: Existing tests still pass
  4. Add tests for retry behavior

    • File: src/lib/SessionSummaryService.test.ts
    • Add test cases for: thrown error retry, result-string retry, non-matching error no-retry, generateSummary retry, double-failure throw
    • Verify: All tests pass with pnpm test:single -- src/lib/SessionSummaryService.test.ts
  5. Build

    • Run pnpm build to verify TypeScript compiles
    • Verify: Clean build

Dependencies and Configuration

None

acreeger added 2 commits April 2, 2026 09:09
…long

When session transcript exceeds Sonnet's 200K context window,
SessionSummaryService now detects the "Prompt is too long" error
and automatically retries with opus[1m] (1M context). This handles
long sessions that would previously fail silently.

Fixes #977
- Fetch staged diff upfront instead of relying on Claude directory access
- Remove addDir option to eliminate unnecessary worktree context
- Add effort: 'low' to generate messages faster with lighter weight
- Truncate diffs exceeding 50KB to prevent token limit issues
- Embed diff as structured XML section in commit prompt

Fixes #977
@acreeger acreeger marked this pull request as ready for review April 2, 2026 13:15
@acreeger acreeger merged commit 4da6501 into main Apr 2, 2026
4 checks passed
@github-project-automation github-project-automation bot moved this to Done in iloom-cli Apr 2, 2026
@acreeger
Copy link
Copy Markdown
Collaborator Author

acreeger commented Apr 2, 2026

iloom Session Summary

Key Themes:

  • Claude Code's --bare flag reduces utility invocation costs by 24x by eliminating MCP server/plugin context loading, but requires careful OAuth token handling since bare mode disables keychain/OAuth auth
  • Embedding diffs directly in prompts (via stdin) eliminates multi-turn tool use, reducing commit message generation from 12-21 turns to 1 turn
  • Long Claude Code sessions can exceed model context windows when resuming for summaries, requiring automatic fallback to larger context models

Session Details (click to expand)

Key Insights

  • --bare mode sets CLAUDE_CODE_SIMPLE=1 — this is the exact same env var that was previously removed in commit 308e55c because it broke OAuth auth. The --bare flag and CLAUDE_CODE_SIMPLE are the same mechanism, so the same auth failure risk applies
  • OAuth tokens are extractable programmatically: on macOS via security find-generic-password -s "Claude Code-credentials" -a "$USER" -w (no Keychain dialog triggered), on Linux via ~/.claude/.credentials.json. Both return JSON with claudeAiOauth.accessToken
  • No Claude Code CLI command outputs the stored OAuth token — the auth status --json command intentionally omits it. CLAUDE_CODE_OAUTH_TOKEN env var and setup-token (creates new long-lived token) are the only documented alternatives
  • --bare mode still loads user-configured MCP servers and plugins from ~/.claude/settings.json — this appears to be a Claude Code bug. The --mcp-config flag is additive, not a replacement, so passing an empty config doesn't suppress defaults
  • Bare mode dramatically reduces context token usage: 9,120 cache creation tokens (bare) vs 54,768 (non-bare) = 6x reduction, translating to 24x cost reduction ($0.003 vs $0.071) for a simple branch name generation call
  • --system-prompt replaces the default system prompt entirely in bare mode (since CLAUDE.md auto-discovery is disabled). --append-system-prompt appends to the default, which doesn't exist in bare mode
  • launchClaude passes prompts via execa's input option (stdin pipe), not CLI args. No practical size limit — stdin pipes are kernel-buffered. The real limit is the model's context window
  • "Prompt is too long" from Claude Code can manifest as either a thrown error OR a returned result string — both paths must be handled when implementing retry logic
  • --resume with sessionId loads the full conversation transcript into context — for long sessions this alone can exceed the model's context window, independent of whether --bare is used
  • expiresAt in Claude Code credentials is stored in milliseconds (confirmed: values like 1774917541773), but a heuristic for seconds-format (value < 10 billion) was added defensively

Decisions Made

  • Centralized bare mode in launchClaude() rather than at each call site — when headless && noSessionPersistence && bare === undefined, bare mode auto-applies. This means all 9+ utility invocation sites get the optimization without code changes. Callers can still explicitly set bare: false to opt out
  • OAuth tokens passed via __ILOOM_OAUTH_TOKEN environment variable, not embedded in CLI args — CLI args are visible to all users via ps aux, making token exposure a security risk. The apiKeyHelper is set to echo $__ILOOM_OAUTH_TOKEN which reads the env var at runtime
  • Pre-fetch git diff --staged and embed in commit message prompt rather than letting Claude explore the repo via tools — this eliminates the multi-turn exploration pattern where Claude runs git status, git diff, reads files, etc. The diff is passed as part of the user message via stdin
  • shouldUseBareMode() renamed to hasApiKeyForBareMode() to reflect that it only checks ANTHROPIC_API_KEY, not OAuth tokens. The full async check is resolveBareModeConfig() which also handles OAuth extraction
  • Retry without --bare only when bare was auto-applied, tracked via bareModeAutoApplied flag. If the caller explicitly set bare: true, auth failures are their responsibility — silently changing intent would be a worse bug than a visible error
  • Summary generation falls back to opus[1m] (1M context) when the configured model hits "Prompt is too long". The model string opus[1m] is a valid entry in VALID_CLAUDE_MODELS at SettingsManager.ts:21

Challenges Resolved

  • OAuth token in process args was a security vulnerability — the initial implementation embedded the token directly in --settings '{"apiKeyHelper": "printf TOKEN"}', visible via ps aux. Resolved by passing the token through an environment variable instead
  • Commit message generation used 12-21 tool-use turns — Claude was running git status, git diff --staged (output too large at 66KB), trying to Read the persisted output (exceeded 10K token limit), then exploring individual files. Root cause: the diff wasn't in the prompt, so Claude had to discover it via tools. Embedding the diff eliminated all tool use
  • generateBranchName() third parameter is model, not description — the test command was passing a description string as the model parameter, causing --model "Users have requested a dark mode toggle..." in the CLI args. TypeScript didn't catch this because both are string type
  • executeGitCommand second parameter is an options object, not a string — passing worktreePath directly instead of { cwd: worktreePath } caused TS2559: Type 'string' has no properties in common with type...

Lessons Learned

  • Cost optimization for AI utility calls has two dimensions: reducing context tokens (via --bare skipping tools/plugins/CLAUDE.md) and reducing turns (via embedding data in prompts). Both are multiplicative — bare mode saved 6x on tokens per turn, and single-turn saved 12-21x on turns
  • --effort low doesn't reduce Claude Code tool use turns — it was expected to reduce exploration but actually had no effect or made it worse (21 turns at medium vs 12 at low). The only reliable way to eliminate turns was removing the need for tools entirely
  • Commander.js hides commands via { hidden: true } option passed to .command(), not via a method chain. hideHelp() exists on Option but not Command. The deprecated noHelp: true was renamed to hidden in Commander v5.1
  • Pre-commit hooks run against the entire working tree, not just staged files — lint and typecheck errors in unstaged files will block commits of unrelated staged files. This is a common source of friction when partially staging changes

Generated with 🤖❤️ by iloom.ai

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

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Implment --bare flag on branch naming and commit message generation claude cli invocations

1 participant