Skip to content

Remove Think's Git identity entirely - #34

Open
flyingrobots wants to merge 2 commits into
mainfrom
fix/no-identity-writes-into-host-repo
Open

Remove Think's Git identity entirely#34
flyingrobots wants to merge 2 commits into
mainfrom
fix/no-identity-writes-into-host-repo

Conversation

@flyingrobots

@flyingrobots flyingrobots commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Do not merge yet. This branch is deliberately red and blocked on git-stunts/plumbing#13. See Blocked below.

What this does

Think writes no Git objects. commit, commit-tree, write-tree, hash-object and update-ref appear nowhere in src/. Every mind commit is created by git-warp through git commit-tree, which takes no author argument and reads only ambient state.

Supplying an identity was therefore never Think's job, and nothing reads one back — no surface queries %an, %ae, %cn or %ce. So Think stops having one:

  • THINK_GIT_CONFIG_ARGS returns to core.fsmonitor=false alone
  • DEFAULT_GIT_ENV is deleted — dead weight regardless, since runGit only ever runs init and config, neither of which creates a commit, and it was never applied to the warp runner, so it never reached a single commit object
  • ensureGitRepo no longer writes user.name/user.email into the target repository

Why the original defect mattered

ensureGitRepo wrote identity into whatever directory it was handed. A THINK_REPO_DIR aimed at a source checkout silently rewrote that repository's committer identity, and every commit made by hand there afterwards was authored as think@local.invalid — a reserved TLD (RFC 2606) that no forge can attribute to an account.

This was not hypothetical. It happened to this repository; the [user] section sat in its config next to the core.fsmonitor value the same function sets. It is the direct cause of every BLOCKED merge here recently, because GitHub reported those commits verified: false / no_user and the required_signatures rule treats that as unsigned.

Blocked

test/ports/git-identity-isolation.test.js asserts a capture carries the operator's configured identity. That currently fails — 187/188, both ratchets clean — and the failure is the acceptance gate, not a defect to work around.

@git-stunts/plumbing filters git's environment through EnvironmentPolicy, whose allowlist contains no HOME and no GIT_CONFIG_GLOBAL. Git cannot read ~/.gitconfig and fabricates user@hostname instead. plumbing#13 fixes that and is approved, but unmerged and unpublished.

Merging this before the fix ships would regress attribution from think@local.invalid to james@Jamess-MacBook-Pro-2.local — different garbage, not an improvement. Verified in both directions by patching node_modules and restoring it.

Merge order: plumbing#13 merged → plumbing published → Think's exact 3.0.3 pin raised (see #37) → this branch green → merge.

Correction

5d8c937's message claims that without an injected identity "git would refuse to commit and the capture would fail outright." That is false. Git auto-detects from the gecos field and hostname and commits successfully. The mutation check cited there did fail, but on the author string, not on the capture breaking — a mechanism asserted before it was checked. Corrected in 94bb8e7, since commits are not amended here.

Follow-ups filed

ensureGitRepo wrote user.name and user.email into whatever directory it was
handed. That directory is not always a Think mind: point THINK_REPO_DIR at a
source checkout, or run a test that resolves the repo root, and the
checkout's own committer identity is silently replaced.

The damage outlives the process. Every commit made by hand in that checkout
afterwards is authored as think@local.invalid, a reserved .invalid address
no forge can attribute to an account, so GitHub reports the commits as
unverified with reason "no_user" even when the signature itself is good.
A branch protection rule requiring signatures then blocks every pull request
containing them.

This was not hypothetical: it happened to this repository, and the
fingerprint is still in its config history — the [user] section sits between
two branch entries written during the work, alongside the core.fsmonitor
value the same function sets.

The identity is per-invocation state, so it now rides on THINK_GIT_CONFIG_ARGS
as -c user.name / -c user.email. Those args already flow through
withThinkGitConfig into both the direct runGit path and git-warp's runner, so
both keep committing as the agent while nothing is persisted.

Verified end to end with env -i and no stored identity: a real capture
produced three warp commits authored and committed as
think <think@local.invalid>, and the mind's config carries no user.* at all.

test/ports/git-identity-isolation.test.js pins both halves. Before the change
the host-repository test reported actual 'think' against expected
'Host Developer'.
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Summary by CodeRabbit

  • Bug Fixes

    • Git operations now use an invocation-specific identity without changing saved repository author or committer settings.
    • Existing repository identity configuration is preserved, and newly created repositories receive no Think identity configuration.
    • Commits created by Think are consistently attributed to Think rather than the host developer.
  • Documentation

    • Added an Unreleased changelog entry describing the Git identity isolation improvements.

Walkthrough

Git identity configuration is centralized and passed to Git per invocation. ensureGitRepo no longer writes identity values to repository configuration. Tests verify preservation, isolation, and commit attribution.

Changes

Git identity isolation

Layer / File(s) Summary
Per-invocation identity configuration
src/git.js
THINK_IDENTITY centralizes Think’s name and email. Git config arguments and author/committer environment values use this identity. ensureGitRepo no longer persists identity settings.
Identity isolation validation
test/ports/git-identity-isolation.test.js, CHANGELOG.md
Tests isolate Git configuration and environment values, verify existing identities remain unchanged, confirm new repositories have no identity configuration, and validate Think commit attribution. The changelog records the behavior.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Think
  participant ensureGitRepo
  participant Git
  participant TargetRepository
  Think->>ensureGitRepo: initialize target repository
  ensureGitRepo->>Git: invoke with per-call identity configuration
  Git->>TargetRepository: initialize or inspect repository
  Think->>Git: run commit with shared identity environment
  Git->>TargetRepository: create commit without persistent identity writes
Loading

Possibly related PRs

Poem

Think’s identity travels light,
Git receives it per command.
Repository settings stay unchanged,
Commits bear the intended name,
Tests guard the boundary tight.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title addresses Git identity removal, which is central, but “entirely” overstates the change because Think still passes identity per invocation.
Description check ✅ Passed The description directly discusses Git identity isolation, repository configuration changes, commit attribution, and the related 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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 00a8503572

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +99 to +101
const commit = spawnSync(
'git',
['-C', mindDir, 'commit', '-q', '-m', 'thought'],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Exercise the Think runner before accepting identity failure

This invokes plain git commit rather than the plumbing created above, so it never exercises the new THINK_GIT_CONFIG_ARGS path; moreover, the expected no-identity failure is treated as a successful test return, while a successful commit only has to use any identity other than the host fixture. Consequently, removing or breaking the runner's user.* arguments would still pass this regression test even though captures on machines without configured Git identity would fail. Drive an actual commit through Think's runner and assert exactly think <think@local.invalid>.

AGENTS.md reference: AGENTS.md:L53-L54

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Around line 18-19: Update the changelog text around the `think@local.invalid`
reference to distinguish account attribution from cryptographic validity: state
that the persisted commit email prevents forge account attribution for later
signed commits, unless a forge-configured no-reply address is used and supported
by the signing configuration.

In `@test/ports/git-identity-isolation.test.js`:
- Around line 91-124: Update the test around createThinkPlumbing to invoke
Think’s commit path through the returned plumbing instance instead of spawning
bare git commit. Require that operation to succeed without accepting or
returning on identity failure, then inspect the resulting commit and assert both
author and committer are exactly think <think@local.invalid>.
- Around line 78-82: Extend the assertions in the repository configuration test
to verify that readLocalConfig(mindDir, 'user.name') also returns null,
alongside the existing user.email assertion. Keep the test’s expected absence
behavior and explanatory assertion context consistent for both identity fields.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f60a29e2-7902-4c2d-b32f-6f1d550da459

📥 Commits

Reviewing files that changed from the base of the PR and between 0f1049c and 00a8503.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • src/git.js
  • test/ports/git-identity-isolation.test.js
📜 Review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: test (22)
🧰 Additional context used
🪛 ast-grep (0.45.0)
test/ports/git-identity-isolation.test.js

[warning] 1-1: Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawnSync } from 'node:child_process';
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process)

🔇 Additional comments (3)
src/git.js (2)

60-68: LGTM!


39-43: 🗄️ Data Integrity & Integration

No change needed. The default git-warp plumbing commit path sets Git author/committer identity through GitPersistenceService in its child environment, so -c user.name alone is not the only isolation mechanism.

			> Likely an incorrect or invalid review comment.
test/ports/git-identity-isolation.test.js (1)

54-70: LGTM!

Comment thread CHANGELOG.md Outdated
Comment thread test/ports/git-identity-isolation.test.js
Comment on lines +91 to +124
const plumbing = createThinkPlumbing(mindDir);
assert.ok(plumbing, 'Expected Think plumbing for the mind.');

// Drive a commit through the same runner Think uses, with no identity in the
// repository config and none inherited from the environment.
const result = spawnSync('git', ['-C', mindDir, 'add', '-A'], { encoding: 'utf8' });
assert.equal(result.status, 0, `git add failed: ${result.stderr}`);

const commit = spawnSync(
'git',
['-C', mindDir, 'commit', '-q', '-m', 'thought'],
{
encoding: 'utf8',
env: stripIdentityEnv(process.env),
}
);

// Without a stored identity and without env identity, git refuses to commit —
// which is exactly why the identity has to travel with each invocation.
if (commit.status !== 0) {
assert.match(
`${commit.stderr}`,
/Please tell me who you are|unable to auto-detect email/u,
`Unexpected commit failure: ${commit.stderr}`
);
return;
}

const author = git(mindDir, ['log', '-1', '--format=%an <%ae>']);
assert.notEqual(
author,
`${HOST_NAME} <${HOST_EMAIL}>`,
'A mind commit must not be attributed to the host developer.'
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Test the Think commit path instead of bare Git.

Lines 91-124 create plumbing but never use it. The test runs bare git commit, accepts the expected identity failure, and returns. It therefore passes without proving that Think can commit or that Think uses think <think@local.invalid>.

Create the commit through the createThinkPlumbing path. Require the commit to succeed. Assert the exact author and committer identities.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/ports/git-identity-isolation.test.js` around lines 91 - 124, Update the
test around createThinkPlumbing to invoke Think’s commit path through the
returned plumbing instance instead of spawning bare git commit. Require that
operation to succeed without accepting or returning on identity failure, then
inspect the resulting commit and assert both author and committer are exactly
think <think@local.invalid>.

CodeRabbit was right about the third test: it drove a plain `git commit`
rather than Think's runner, and treated the no-identity failure as a pass.
Removing the runner's -c arguments would have left it green while every
capture on a machine without a configured git identity broke.

It now runs a real capture through the CLI with every ambient identity
source removed — no repository config, GIT_CONFIG_GLOBAL and
GIT_CONFIG_SYSTEM pointed at /dev/null, and GIT_AUTHOR_*/GIT_COMMITTER_*
stripped — and asserts every resulting commit is exactly
`think <think@local.invalid>` on both the author and committer side, with
nothing persisted to the mind's config.

Mutation-checked rather than assumed: with the -c arguments removed the new
test fails and the old one passed.

Also asserts user.name is absent alongside user.email, so persisting either
half reintroduces the leak.

The changelog conflated two different things. A wrong user.email does not
invalidate a signature — the commits stay cryptographically valid. What
breaks is attribution: `.invalid` is reserved, so no forge can match the
address to an account and GitHub reports `verified: false` / `no_user`,
which a rule requiring signed commits treats as unsigned. Said that way now.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant