Skip to content

Allow retrying git commits without signing when signing fails#4219

Closed
tim-smart wants to merge 13 commits into
pingdotgg:mainfrom
tim-smart:t3code/git-signing-toggle
Closed

Allow retrying git commits without signing when signing fails#4219
tim-smart wants to merge 13 commits into
pingdotgg:mainfrom
tim-smart:t3code/git-signing-toggle

Conversation

@tim-smart

@tim-smart tim-smart commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Previously, when git signing failed (due to non-interactive environments etc),
the git commit would be aborted.

With this change, it allows you to retry with signing disabled.


Note

High Risk
Touches detached process spawning, provider session environments, git commit/signing paths, and new desktop IPC—multiple security- and workflow-sensitive areas in one PR.

Overview
This PR bundles several capabilities beyond the titled commit-signing work: desktop “Open With”, direnv-backed provider environments, and git commit signing retry with related server/git fixes.

Open With (desktop, macOS-focused) adds configurable client settings for launching folders in Terminal, other commands, or .app bundles. The Electron layer gains pickApplication (Applications picker + optional icon via MacApplicationIcon), IPC/preload hooks (pickOpenWithApplication, resolveOpenWithPresentations, openWith), and DesktopOpenWith to resolve availability, icons, and detached launches (open -a, cwd/argument modes). Tests cover dialog, IPC, and launch resolution.

Direnv introduces DirenvEnvironment on the server: discover nearest .envrc, run direnv export json with an exact non-extending env (extendEnv: false on processRunner), redact secrets in errors, and allow only a worktree-root .envrc. All shipped provider drivers pass resolveEnvironment into adapters so local session cwd gets a merged env (Claude/Codex/Cursor/Grok/OpenCode); external OpenCode URLs skip resolution.

Git commit signing retry adds disableCommitSigning through stacked actions, classifies signing failures (failureKind: commit_signing_failed), and on mobile shows an Alert to retry via buildUnsignedCommitRetryInput. GitManager improves commit hook output attribution, forwards the unsigned override, and adds tests for signing failure/retry behavior. initRepo tests disable GPG sign by default.

Risk note: Large surface area—spawn/env handling, new desktop IPC, and git workflow changes.

Reviewed by Cursor Bugbot for commit 90c665d. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add unsigned commit retry when GPG signing fails in git commit workflow

  • Introduces commit_signing_failed as a classified GitActionFailureKind, detected by matching stderr against known GPG, pinentry, and SSH signing error patterns in GitVcsDriverCore.
  • Adds disableCommitSigning flag through the full stack: contracts, server-side GitManager/GitVcsDriver, client-runtime vcsAction, and UI hooks, allowing a single commit attempt to bypass GPG signing via --no-gpg-sign.
  • On desktop, GitActionsControl detects signing failures and updates the toast with a

🖇️ Linked Issues

"Retry without signing" action that re-runs the commit with disableCommitSigning: true.

  • On mobile, use-selected-thread-git-actions shows a native Alert prompting the user to retry without signing.
  • Also ships a large OpenInPicker overhaul and DirenvEnvironment integration for per-session environment resolution, but the signing retry is the primary behavioral change.
  • Risk: commits retried without signing will not carry GPG signatures; extendEnv defaulting changes in processRunner mean processes launched without an explicit env no longer inherit the parent environment by default."AC_linked_issues":""}

Macroscope summarized 90c665d.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jul 21, 2026
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cf20c530-b5f2-477d-a7f6-6a82c3b2976b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@tim-smart tim-smart changed the title Allow retrying git commits with signing when signing fails Allow retrying git commits without signing when signing fails Jul 21, 2026
Comment thread apps/mobile/src/state/use-selected-thread-git-actions.ts
Comment thread packages/contracts/src/git.ts
Comment thread apps/mobile/src/state/use-selected-thread-git-actions.ts Outdated
Comment thread packages/contracts/src/git.ts
@macroscopeapp

macroscopeapp Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

6 blocking correctness issues found. Diff is too large for automated approval analysis. A human reviewer should evaluate this PR.

You can customize Macroscope's approvability policy. Learn more.

Comment thread apps/server/src/git/GitManager.ts
@tim-smart
tim-smart force-pushed the t3code/git-signing-toggle branch 2 times, most recently from 2de8119 to 779bf4e Compare July 22, 2026 21:13
const failure: E | VcsActionRemoteFailureError =
terminal?.kind === "action_failed" ? remoteFailure(terminal) : error;
return Effect.fail<E | VcsActionRemoteFailureError>(failure);
}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Success lost after stream error

Medium Severity

The new Effect.catch prefers a terminal action_failed over a trailing stream error, but still fails when action_finished was already received. A successful stacked action (including an unsigned retry) can surface as a failure if the RPC stream errors after the terminal event, so the UI can prompt another attempt and create a duplicate commit.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 779bf4e. Configure here.

});
currentHookName = null;
}
yield* finalizeUnattributedOutput(sawCommitHook);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hookless failures hide git stderr

Medium Severity

Unattributed commit output is buffered while no hook is active, then flushed only when sawCommitHook is true. In repos without commit hooks, non-signing commit failures never emit those stderr lines as progress, so the UI keeps a generic “Git command exited with a non-zero status” message instead of the underlying git diagnostic.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 779bf4e. Configure here.

Comment thread apps/web/src/components/GitActionsControl.tsx
@tim-smart
tim-smart force-pushed the t3code/git-signing-toggle branch 4 times, most recently from 63e8682 to c7907a7 Compare July 24, 2026 02:51
tim-smart added 13 commits July 24, 2026 14:51
- Let users pick macOS .app bundles with system icons
- Add open-with IPC, launch resolution, and settings plumbing
- Cover bundle parsing, availability, and launch behavior with tests
- Share Effect-based icon resolution across dialogs and Open With
- Prefer bundle ICNS assets with Electron fallback
- Make the application path field editable and clear stale icon and validation state on change
- Apply cwd-specific `.envrc` changes across supported provider runtimes
- Preserve provider invariants and surface safe, actionable direnv errors
- Run `direnv allow` for a root `.envrc` after creating a worktree
- Keep worktree creation successful when approval fails
- Resolve approved direnv variables for provider processes and Git commands
- Share live and no-op direnv layers with cached executable lookup
- Detect commit signing failures across server and client
- Let stacked git actions retry once with signing disabled
- Preserve unsigned commit behavior in commit, push, and PR flows
- Prevent stale action controls from appearing on Git progress toasts
@tim-smart
tim-smart force-pushed the t3code/git-signing-toggle branch from c7907a7 to 90c665d Compare July 24, 2026 03:34
@github-actions github-actions Bot removed the size:L 100-499 changed lines (additions + deletions). label Jul 24, 2026
@github-actions github-actions Bot added the size:XXL 1,000+ changed lines (additions + deletions). label Jul 24, 2026
});
export type OpenWithEntry = typeof OpenWithEntry.Type;

export const OpenWithEntries = Schema.Array(OpenWithEntry).check(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium src/openWith.ts:54

OpenWithEntries validates array length but does not enforce uniqueness of OpenWithEntry.id, so a decoded collection can contain multiple entries with the same id. Callers resolve entries with .find(candidate => candidate.id === input.entryId), which returns the first match — meaning when a duplicate id exists, launching the later entry silently launches the earlier one instead. Consider adding a uniqueness-by-id filter to the schema.

🤖 Copy this AI Prompt to have your agent fix this:
In file @packages/contracts/src/openWith.ts around line 54:

`OpenWithEntries` validates array length but does not enforce uniqueness of `OpenWithEntry.id`, so a decoded collection can contain multiple entries with the same `id`. Callers resolve entries with `.find(candidate => candidate.id === input.entryId)`, which returns the first match — meaning when a duplicate `id` exists, launching the later entry silently launches the earlier one instead. Consider adding a uniqueness-by-`id` filter to the schema.

);
});

const entryIsAvailable = Effect.fn("desktop.openWith.entryIsAvailable")(function* (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium shell/DesktopOpenWith.ts:59

entryIsAvailable marks any directory ending in .app as available without checking for Contents/Info.plist or a valid executable. In open-target mode the code passes the path straight to /usr/bin/open with no further validation, so an incomplete or fake .app directory is reported as available and open can succeed after detaching even though nothing actually launches. Consider validating the bundle structure (e.g. calling resolveMacBundleExecutable) before reporting the entry as available.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/desktop/src/shell/DesktopOpenWith.ts around line 59:

`entryIsAvailable` marks any directory ending in `.app` as available without checking for `Contents/Info.plist` or a valid executable. In `open-target` mode the code passes the path straight to `/usr/bin/open` with no further validation, so an incomplete or fake `.app` directory is reported as available and `open` can succeed after detaching even though nothing actually launches. Consider validating the bundle structure (e.g. calling `resolveMacBundleExecutable`) before reporting the entry as available.

Comment on lines +17 to +19
export function readLegacyPreferredEditor(): EditorId | null {
return getLocalStorageItem(LAST_EDITOR_KEY, EditorId);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium src/editorPreferences.ts:17

readLegacyPreferredEditor calls getLocalStorageItem directly, so a malformed or stale t3code:last-editor value causes OpenInPicker to throw LocalStorageOperationError during render instead of falling back. Unlike useLocalStorage, which catches decode failures, this path has no error handling. Wrap the call in a try/catch and return null on failure so invalid stored values fall back gracefully.

Suggested change
export function readLegacyPreferredEditor(): EditorId | null {
return getLocalStorageItem(LAST_EDITOR_KEY, EditorId);
}
export function readLegacyPreferredEditor(): EditorId | null {
try {
return getLocalStorageItem(LAST_EDITOR_KEY, EditorId);
} catch {
return null;
}
}
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/editorPreferences.ts around lines 17-19:

`readLegacyPreferredEditor` calls `getLocalStorageItem` directly, so a malformed or stale `t3code:last-editor` value causes `OpenInPicker` to throw `LocalStorageOperationError` during render instead of falling back. Unlike `useLocalStorage`, which catches decode failures, this path has no error handling. Wrap the call in a try/catch and return `null` on failure so invalid stored values fall back gracefully.

const findDirenv = Effect.fn("DirenvEnvironment.findDirenv")(function* (
environment: NodeJS.ProcessEnv,
) {
const cacheKey = environment.PATH ?? "";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium provider/DirenvEnvironment.ts:123

findDirenv uses only environment.PATH as its cache key, but on Windows command resolution also reads the case-insensitive Path key and depends on PATHEXT. Two environments that both omit PATH (or share the same PATH value) but differ in Path/PATHEXT collide in the cache. If the first lookup resolves to undefined, a later session whose Path actually contains direnv gets the cached undefined, so direnv is never invoked and its .envrc environment is silently skipped. Consider building the cache key from the normalized, case-insensitive PATH and including PATHEXT (or avoid caching across distinct environments).

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/DirenvEnvironment.ts around line 123:

`findDirenv` uses only `environment.PATH` as its cache key, but on Windows command resolution also reads the case-insensitive `Path` key and depends on `PATHEXT`. Two environments that both omit `PATH` (or share the same `PATH` value) but differ in `Path`/`PATHEXT` collide in the cache. If the first lookup resolves to `undefined`, a later session whose `Path` actually contains `direnv` gets the cached `undefined`, so direnv is never invoked and its `.envrc` environment is silently skipped. Consider building the cache key from the normalized, case-insensitive PATH and including `PATHEXT` (or avoid caching across distinct environments).


if (entry.directoryMode === "open-target") {
if (entry.invocation.type === "mac-application") {
return {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium shell/DesktopOpenWith.ts:156

The open-target branch for mac-application entries builds the open command as /usr/bin/open -a <app> <directory>, silently discarding entry.arguments. A mac-application entry saved with custom arguments launches without any of those arguments reaching the application, while the command branch and both other directory modes all forward configured arguments. Consider including entry.arguments in the open invocation, e.g. via --args.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/desktop/src/shell/DesktopOpenWith.ts around line 156:

The `open-target` branch for `mac-application` entries builds the `open` command as `/usr/bin/open -a <app> <directory>`, silently discarding `entry.arguments`. A mac-application entry saved with custom arguments launches without any of those arguments reaching the application, while the `command` branch and both other directory modes all forward configured arguments. Consider including `entry.arguments` in the `open` invocation, e.g. via `--args`.

options.spawn.command,
options.spawn.args,
options.spawn.env ? { env: options.spawn.env, extendEnv: true } : {},
options.spawn.env ? { env: options.spawn.env, extendEnv } : {},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium acp/AcpSessionRuntime.ts:337

When options.spawn.env is omitted, extendEnv: false is silently ignored — the spawned ACP process still inherits the parent environment. Both call sites that would use extendEnv replace the entire options object with {} whenever env is undefined, so the flag never reaches resolveSpawnCommand or ChildProcess.make. A caller who sets only extendEnv: false (without env) does not get an isolated environment. Pass extendEnv independently of env, or require env whenever extendEnv is false.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/acp/AcpSessionRuntime.ts around line 337:

When `options.spawn.env` is omitted, `extendEnv: false` is silently ignored — the spawned ACP process still inherits the parent environment. Both call sites that would use `extendEnv` replace the entire options object with `{}` whenever `env` is undefined, so the flag never reaches `resolveSpawnCommand` or `ChildProcess.make`. A caller who sets only `extendEnv: false` (without `env`) does not get an isolated environment. Pass `extendEnv` independently of `env`, or require `env` whenever `extendEnv` is `false`.

@tim-smart tim-smart closed this Jul 24, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 90c665d. Configure here.

entryId: entry.id,
executable: "Custom arguments must include {directory}",
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Wrong error for bad arguments

Low Severity

custom-arguments entries missing {directory} fail as OpenWithUnavailableApplicationError with the validation text stuffed into executable. Users see an “application unavailable” message for a configuration problem, which misstates the failure mode.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 90c665d. Configure here.

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

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant