Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .github/workflows/gitmind-suggest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: git-mind Suggest

on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
branches: [main]

concurrency:
group: gitmind-suggest-${{ github.event.pull_request.number }}
cancel-in-progress: true

permissions:
contents: read
pull-requests: write

jobs:
suggest:
if: >
!github.event.pull_request.draft &&
github.actor != 'dependabot[bot]'
Comment on lines +18 to +20
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Bot guard only excludes dependabot[bot] — other automation actors will consume API quota.

renovate[bot], github-actions[bot], and custom bots are not excluded. Unless intentional, tighten the guard.

🤖 Proposed fix
     if: >
       !github.event.pull_request.draft &&
-      github.actor != 'dependabot[bot]'
+      github.actor != 'dependabot[bot]' &&
+      !endsWith(github.actor, '[bot]')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if: >
!github.event.pull_request.draft &&
github.actor != 'dependabot[bot]'
if: >
!github.event.pull_request.draft &&
github.actor != 'dependabot[bot]' &&
!endsWith(github.actor, '[bot]')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/gitmind-suggest.yml around lines 18 - 20, The current
GitHub Actions workflow if condition only excludes 'dependabot[bot]' which
allows other automation actors to trigger the job and consume API quota; update
the if condition used in the workflow (the if: > block) to also exclude known
automation actors such as 'renovate[bot]' and 'github-actions[bot]' (and any
organization-specific bot names you use) alongside 'dependabot[bot]' so that
those actors are skipped when evaluating the job run.

🧹 Nitpick | 🔵 Trivial

Bot guard only excludes dependabot[bot] — other automation actors will consume API credits.

renovate[bot], github-actions[bot], and any custom bot actors are not excluded. Unless intentional, add a broader guard.

🤖 Proposed fix
     if: >
       !github.event.pull_request.draft &&
-      github.actor != 'dependabot[bot]'
+      github.actor != 'dependabot[bot]' &&
+      github.actor != 'renovate[bot]' &&
+      !endsWith(github.actor, '[bot]')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/gitmind-suggest.yml around lines 18 - 20, The workflow
currently only excludes 'dependabot[bot]'; update the if conditional in the
gitmind-suggest.yml workflow (the "if:" expression) to broadly exclude bot
actors by replacing or extending the github.actor check with a suffix test such
as !endsWith(github.actor, '[bot]') (e.g. use "!github.event.pull_request.draft
&& !endsWith(github.actor, '[bot]')") so renovate[bot], github-actions[bot], and
other bot actors are skipped as well.

runs-on: ubuntu-latest
continue-on-error: true

steps:
- name: Check for API key
id: check-key
env:
HAS_KEY: ${{ secrets.ANTHROPIC_API_KEY != '' }}
run: |
if [ "$HAS_KEY" != "true" ]; then
echo "::notice::ANTHROPIC_API_KEY not configured — skipping suggest"
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi

- name: Checkout
if: steps.check-key.outputs.skip != 'true'
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup Node.js
if: steps.check-key.outputs.skip != 'true'
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'

- name: Install dependencies
if: steps.check-key.outputs.skip != 'true'
run: npm ci

- name: Install Claude CLI
if: steps.check-key.outputs.skip != 'true'
run: npm install -g @anthropic-ai/claude-code
Comment on lines +54 to +56
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

@anthropic-ai/claude-code npm package latest version

💡 Result:

The latest published version of @anthropic-ai/claude-code on npm is 1.0.108. [1]

Source: npm package “Versions” tab shows Version 1.0.108 and “Last publish 5 hours ago.” [1]

References

  1. @anthropic-ai/claude-code (npm, versions tab)

Pin @anthropic-ai/claude-code to an exact version; unpinned global installs are indefensible.

Line 56: npm install -g @anthropic-ai/claude-code`` fetches whatever the registry serves on the next run. One semver bump (malicious or accidental) and your workflow executes unknown code with ANTHROPIC_API_KEY access. No audit trail, no rollback, silent failure. Current stable is `1.0.108` — pin it and let Dependabot handle updates.

Fix
-        run: npm install -g `@anthropic-ai/claude-code`
+        run: npm install -g `@anthropic-ai/claude-code`@1.0.108
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Install Claude CLI
if: steps.check-key.outputs.skip != 'true'
run: npm install -g @anthropic-ai/claude-code
- name: Install Claude CLI
if: steps.check-key.outputs.skip != 'true'
run: npm install -g `@anthropic-ai/claude-code`@1.0.108
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/gitmind-suggest.yml around lines 54 - 56, The workflow
currently installs `@anthropic-ai/claude-code` without a fixed version; update the
install step so the global npm install pins the package to the exact safe
version (e.g., change the command that currently reads npm install -g
`@anthropic-ai/claude-code` to install the exact release, such as npm install -g
`@anthropic-ai/claude-code`@1.0.108) to prevent unintentionally running unvetted
code and allow Dependabot to manage future updates.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Anthropic Claude CLI npm deprecated official documentation

💡 Result:

Anthropic’s official Claude Code (Claude CLI) docs state that “NPM installation is deprecated” and recommend using the native installation method instead; to migrate an existing npm install to native, run claude install. The same page still shows the legacy command npm install -g @anthropic-ai/claude-code`` under “NPM installation (deprecated)”. [1]

Recent Claude CLI release notes also mention an added deprecation notification for npm installations, pointing users to run claude install and to the same “getting started” documentation page. [2]

Sources:
[1] Anthropic Claude Code Docs – “Set up Claude Code” → “NPM installation (deprecated)”
[2] Claude CLI Release Notes (notes referencing npm-install deprecation notice)


🌐 Web query:

@anthropic-ai/claude-code npm package latest version

💡 Result:

The latest published version of the @anthropic-ai/claude-code npm package is 1.0.108 (last published ~5 hours ago). [1]

Source: npm package “Versions” tab. [1]


🌐 Web query:

Anthropic Claude CLI native installer setup github

💡 Result:

  • Official GitHub repo (Claude Code CLI): anthropics/claude-code (README includes install commands and project details). [1]
  • Native (binary) installer setup (Beta): Anthropic’s official setup docs recommend installing via the hosted installer scripts (not a GitHub “native installer” release asset): [2]
    • macOS / Linux / WSL: curl -fsSL https://claude.ai/install.sh | bash
    • Windows (PowerShell): irm https://claude.ai/install.ps1 | iex
    • You can also pin a version (e.g., ... | bash -s 1.0.58) and, if you already have Claude Code installed, start native install via claude install. [2]
  • Alternative install path (Node): npm install -g @anthropic-ai/claude-code`` (also documented in the repo + docs). [1][2]

🌐 Web query:

Claude CLI binary path installation $HOME/.claude/bin official documentation

💡 Result:

Anthropic’s official Claude Code docs do not state that the CLI binary installs to $HOME/.claude/bin. In the official docs, ~/.claude/ is used for Claude Code’s user configuration, while the claude executable comes from your install method. (docs.anthropic.com)

Official install methods & where the claude binary comes from:

  • Global npm install (standard):

    • Install: npm install -g @anthropic-ai/claude-code`` (docs.anthropic.com)
    • Binary path is your global npm “bin” directory (varies by system). To see the exact location:
      which claude
      npm prefix -g
      ls -l "$(npm prefix -g)/bin/claude"
  • Native binary install (beta):

    • Install via the official script (macOS/Linux/WSL): curl -fsSL https://claude.ai/install.sh | bash (docs.anthropic.com)
    • After install, Anthropic recommends running:
      claude doctor
      to verify the installation type/version. (docs.anthropic.com)

Citations:


npm install -g @anthropic-ai/claude-code`` is deprecated AND unpinned — this step will break.

"Installation via npm is deprecated. Use one of the recommended methods below." Anthropic's official documentation and release notes are explicit about this. The package itself now ships with a deprecation notification for npm installations.

Beyond deprecation, the install is unpinned — every run silently picks up whatever the registry serves, with no auditability or rollback path if a breaking release lands.

🚑 Proposed fix — switch to the native installer at the current version
      - name: Install Claude CLI
        if: steps.check-key.outputs.skip != 'true'
-       run: npm install -g `@anthropic-ai/claude-code`
+       run: curl -fsSL https://claude.ai/install.sh | bash -s 1.0.108

(The proposed fix in the original comment uses an incorrect version number 2.1.52 and malformed syntax --version; the correct native installer syntax is bash -s VERSION where VERSION is the pinned release like 1.0.108.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/gitmind-suggest.yml around lines 54 - 56, Replace the
deprecated and unpinned npm installation in the "Install Claude CLI" step
(currently using `npm install -g `@anthropic-ai/claude-code``) with the native
installer and pin to a known stable release; specifically, remove the npm
command and invoke the native installer with the correct syntax `bash -s
VERSION`, using a pinned VERSION (for example `1.0.108`) instead of `--version`
so the workflow installs a fixed, auditable Claude CLI release.


- name: Run git-mind suggest
if: steps.check-key.outputs.skip != 'true'
uses: ./
with:
agent: 'claude -p --output-format json'
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
Comment on lines +60 to +64

Choose a reason for hiding this comment

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

P1 Badge Run trusted action code when exposing repository secrets

This step executes the local action from the PR checkout (uses: ./) while injecting ANTHROPIC_API_KEY, and on pull_request events that action code is sourced from the PR head. A contributor can modify action.yml or scripts it runs (including npm ci lifecycle hooks in the composite action) to exfiltrate the secret during CI, so this workflow should run trusted code from the base branch when secrets are present (for example via pull_request_target + trusted checkout) or avoid passing secrets to PR-head code.

Useful? React with 👍 / 👎.

2 changes: 1 addition & 1 deletion bin/git-mind.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ Commands:
--repo-name <owner/name> Override detected repo identifier
--dry-run Preview without writing
--json Output as JSON
install-hooks Install post-commit Git hook
install-hooks Install pre-push Git hook (directives + suggest)
doctor Run graph integrity checks
--fix Auto-fix dangling edges
--at <ref> Time-travel: check graph as-of a git ref
Expand Down
52 changes: 34 additions & 18 deletions src/cli/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -246,44 +246,60 @@ export async function remove(cwd, source, target, opts = {}) {
}
}

/**
* Install a post-commit Git hook that processes directives.
* @param {string} cwd
*/
export async function installHooks(cwd) {
const hookPath = join(cwd, '.git', 'hooks', 'post-commit');
const PRE_PUSH_SCRIPT = `#!/bin/sh
# git-mind pre-push hook
# Processes commit directives and runs suggest on commits about to be pushed

const hookScript = `#!/bin/sh
# git-mind post-commit hook
# Parses commit directives and creates edges automatically
command -v npx >/dev/null 2>&1 || exit 0

SHA=$(git rev-parse HEAD)
MSG=$(git log -1 --format=%B "$SHA")
ZERO="0000000000000000000000000000000000000000"

# Only run if git-mind is available
command -v npx >/dev/null 2>&1 || exit 0
while read local_ref local_sha remote_ref remote_sha; do
[ "$local_sha" = "$ZERO" ] && continue # deleting ref
if [ "$remote_sha" = "$ZERO" ]; then
RANGE="HEAD~10..HEAD" # new branch — use last 10
else
RANGE="\${remote_sha}..\${local_sha}"
fi
Comment on lines +259 to +263
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

HEAD~10..HEAD is the wrong range for new branch pushes — it processes the wrong commits.

When remote_sha is all-zeros (new branch), HEAD is the currently checked-out ref, which is NOT necessarily the branch being pushed. A user can do git push origin feature-branch while sitting on main; every process-commit call and the suggest invocation will run against main's last 10 commits, not feature-branch's.

local_sha is the exact tip being pushed and is always available at this point in the loop.

🐛 Proposed fix
-    RANGE="HEAD~10..HEAD"  # new branch — use last 10
+    RANGE="${local_sha}~10..${local_sha}"  # new branch — use last 10
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/commands.js` around lines 259 - 263, When remote_sha is all zeros
(new branch) the code currently sets RANGE="HEAD~10..HEAD" which picks commits
from the checked-out branch; change that to use the pushed tip instead so we run
against the branch being pushed. Replace the HEAD-based range with
RANGE="${local_sha}~10..${local_sha}" (use the existing local_sha variable) so
process-commit and suggest operate on the last 10 commits ending at local_sha;
keep the else branch for the normal "${remote_sha}..${local_sha}" case.


# Process commit directives for each commit in the range
for SHA in $(git rev-list "$RANGE" 2>/dev/null); do
npx git-mind process-commit "$SHA" 2>/dev/null || true
done

npx git-mind process-commit "$SHA" 2>/dev/null || true
# Run suggest if an agent is configured
if [ -n "$GITMIND_AGENT" ]; then
npx git-mind suggest --context "$RANGE" 2>/dev/null || true
fi
done

exit 0
`;

/**
* Install pre-push Git hook (directives + suggest).
* @param {string} cwd
*/
export async function installHooks(cwd) {
const hookPath = join(cwd, '.git', 'hooks', 'pre-push');

try {
// Check if a hook already exists
let exists = false;
try {
await access(hookPath, constants.F_OK);
exists = true;
} catch { /* doesn't exist */ }

if (exists) {
console.error(error(`Post-commit hook already exists at ${hookPath}`));
console.error(error(`pre-push hook already exists at ${hookPath}`));
console.error(info('Remove it manually or append git-mind to the existing hook'));
process.exitCode = 1;
return;
}

await writeFile(hookPath, hookScript);
await writeFile(hookPath, PRE_PUSH_SCRIPT);
await chmod(hookPath, 0o755);
console.log(success('Installed post-commit hook'));
console.log(success('Installed pre-push hook'));
} catch (err) {
console.error(error(`Failed to install hook: ${err.message}`));
process.exitCode = 1;
Expand Down
68 changes: 67 additions & 1 deletion test/hooks.test.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtemp, rm } from 'node:fs/promises';
import { mkdtemp, rm, readFile, stat, writeFile, mkdir } from 'node:fs/promises';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { execSync } from 'node:child_process';
import { initGraph } from '../src/graph.js';
import { queryEdges } from '../src/edges.js';
import { parseDirectives, processCommit } from '../src/hooks.js';
import { installHooks } from '../src/cli/commands.js';

describe('hooks', () => {
describe('parseDirectives', () => {
Expand Down Expand Up @@ -97,4 +98,69 @@ RELATES-TO: module:session`;
expect(edges.length).toBe(0);
});
});

describe('installHooks', () => {
let tempDir;

beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'gitmind-test-'));
execSync('git init', { cwd: tempDir, stdio: 'ignore' });
await mkdir(join(tempDir, '.git', 'hooks'), { recursive: true });
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Redundant mkdirgit init already creates .git/hooks.

git init at line 107 unconditionally creates .git/hooks. The mkdir with { recursive: true } is a no-op in the happy path and masks any scenario where the directory is unexpectedly absent (which would be a test-environment signal worth catching, not silently creating).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/hooks.test.js` at line 108, Remove the redundant explicit mkdir call
that creates join(tempDir, '.git', 'hooks') after running git init; instead rely
on git init to create the hooks dir and replace the mkdir invocation with an
assertion that the directory exists (or let the test fail if it does not) so
missing directories in the test environment are surfaced—update the test to
reference the same tempDir and check that join(tempDir, '.git', 'hooks') exists
rather than creating it with mkdir.

});

afterEach(async () => {
await rm(tempDir, { recursive: true, force: true });
});

it('creates pre-push hook', async () => {
await installHooks(tempDir);

const prePush = await readFile(join(tempDir, '.git', 'hooks', 'pre-push'), 'utf-8');
expect(prePush).toContain('git-mind suggest');
expect(prePush).toContain('process-commit');
});

it('makes hook executable', async () => {
await installHooks(tempDir);

const prePushStat = await stat(join(tempDir, '.git', 'hooks', 'pre-push'));
expect(prePushStat.mode & 0o111).toBeTruthy();
});

it('does not overwrite existing hook', async () => {
const existingContent = '#!/bin/sh\necho "existing hook"';
await writeFile(join(tempDir, '.git', 'hooks', 'pre-push'), existingContent);

await installHooks(tempDir);

const prePush = await readFile(join(tempDir, '.git', 'hooks', 'pre-push'), 'utf-8');
expect(prePush).toBe(existingContent);
});
Comment on lines +130 to +138
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

process.exitCode side-effect leaks out of this test and will poison the runner's exit code.

installHooks sets process.exitCode = 1 when a hook already exists (see src/cli/commands.js line 296). This test deliberately triggers that path but never restores process.exitCode afterward. Any subsequent test — or the Vitest process itself — will inherit exit code 1, making the suite appear failed even when every assertion passes.

Fix: capture and restore process.exitCode around the call, or assert + reset it explicitly.

🔥 Proposed fix
  it('does not overwrite existing hook', async () => {
    const existingContent = '#!/bin/sh\necho "existing hook"';
    await writeFile(join(tempDir, '.git', 'hooks', 'pre-push'), existingContent);

+   const prevExitCode = process.exitCode;
    await installHooks(tempDir);
+   expect(process.exitCode).toBe(1);
+   process.exitCode = prevExitCode;

    const prePush = await readFile(join(tempDir, '.git', 'hooks', 'pre-push'), 'utf-8');
    expect(prePush).toBe(existingContent);
  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/hooks.test.js` around lines 130 - 138, The test causes installHooks to
set process.exitCode = 1 and never restores it; wrap the call to installHooks
(or capture process.exitCode before the call) and restore it after the assertion
so the global exit code is not leaked. Specifically, in the test case that calls
installHooks, save const originalExitCode = process.exitCode (or undefined), run
installHooks(tempDir), perform the existing assertions on the hook file, then
reset process.exitCode = originalExitCode (or delete it) — alternatively assert
that process.exitCode === 1 and then reset it — to ensure no side-effect remains
after the test.


it('hook runs suggest only when GITMIND_AGENT is set', async () => {
await installHooks(tempDir);

const prePush = await readFile(join(tempDir, '.git', 'hooks', 'pre-push'), 'utf-8');
expect(prePush).toContain('GITMIND_AGENT');
expect(prePush).toMatch(/if \[ -n "\$GITMIND_AGENT" \]/);
});

it('hook always exits 0', async () => {
await installHooks(tempDir);

const prePush = await readFile(join(tempDir, '.git', 'hooks', 'pre-push'), 'utf-8');
expect(prePush).toContain('exit 0');
expect(prePush).toContain('|| true');
});

it('hook processes directives unconditionally', async () => {
await installHooks(tempDir);

const prePush = await readFile(join(tempDir, '.git', 'hooks', 'pre-push'), 'utf-8');
// process-commit runs outside the GITMIND_AGENT guard
const agentGuardIndex = prePush.indexOf('if [ -n "$GITMIND_AGENT" ]');
const processCommitIndex = prePush.indexOf('process-commit');
expect(processCommitIndex).toBeLessThan(agentGuardIndex);
});
});
});
Loading