Skip to content

feat(plugin-github): support unauthenticated mode for public API access#3

Open
odilitime wants to merge 10 commits into1.xfrom
odi-dev
Open

feat(plugin-github): support unauthenticated mode for public API access#3
odilitime wants to merge 10 commits into1.xfrom
odi-dev

Conversation

@odilitime
Copy link
Member

@odilitime odilitime commented Jan 29, 2026

  • GitHubService now supports both authenticated and unauthenticated modes
  • Added authMode property and isAuthenticated() method to service
  • Added requireAuth() method that throws GitHubAuthenticationError for write ops
  • Plugin init no longer throws when GITHUB_TOKEN is missing
  • Actions that require auth check isGitHubAuthenticated() in validate()
  • Added githubStatusProvider to give agent context about capabilities
  • Improved error messages for 403/404 to suggest token when in unauth mode
  • Rate limit errors now show reset time and mode-specific limits

Unauthenticated mode (60 req/hr): view public repos/issues/PRs, search
Authenticated mode (5000 req/hr): full access including private repos and writes


Note

Medium Risk
Adds substantial new GitHub automation (PR comment aggregation/summarization and local git-based PR splitting) plus new auth gating; mistakes could expose tokens via git config or cause unintended git/file operations despite added sanitization.

Overview
Adds token-optional operation: GitHubService now supports unauthenticated public-only mode (60 req/hr) with clearer 403/404/rate-limit messaging, while write operations enforce requireAuth() and actions that create/merge/comment are gated via isAuthenticated().

Expands PR tooling: GET_GITHUB_PULL_REQUEST now fetches and returns issue comments, review comments, and reviews (with bot-friendly formatting) and persists these artifacts; new actions SUMMARIZE_PR_FEEDBACK and AGGREGATE_PR_CONTEXT generate LLM summaries or a copy/paste “mega prompt” of unaddressed review items with caching.

Introduces PR split workflow: new prSplit actions clone a PR branch to a local working copy, analyze commits/files, suggest/refine/confirm split plans, and execute splits by creating local branches with selected files, persisting plans/working-copy metadata for daemon restarts.

Docs and packaging were updated to reflect new env vars/webhook URL strategies, unauthenticated mode behavior, and optional @elizaos/plugin-ngrok/added @elizaos/plugin-git dependency.

Written by Cursor Bugbot for commit b6ad821. This will update automatically on new commits. Configure here.

Summary by CodeRabbit

  • New Features

    • PR-splitting workflow to break large pull requests into smaller branches
    • Local working copy management: clone, list, and delete repositories
    • Aggregated PR feedback and richer PR context (reviews, comments, bot feedback)
    • Activity and GitHub status monitoring plus webhook management
    • Token-aware modes including optional unauthenticated (public-only) mode
    • Console startup banner for config visibility
  • Documentation

    • Revamped Quick Start, env var examples, token/permissions guidance and usage samples

✏️ Tip: You can customize this high-level summary in your review settings.

- GitHubService now supports both authenticated and unauthenticated modes
- Added authMode property and isAuthenticated() method to service
- Added requireAuth() method that throws GitHubAuthenticationError for write ops
- Plugin init no longer throws when GITHUB_TOKEN is missing
- Actions that require auth check isGitHubAuthenticated() in validate()
- Added githubStatusProvider to give agent context about capabilities
- Improved error messages for 403/404 to suggest token when in unauth mode
- Rate limit errors now show reset time and mode-specific limits

Unauthenticated mode (60 req/hr): view public repos/issues/PRs, search
Authenticated mode (5000 req/hr): full access including private repos and writes
Copilot AI review requested due to automatic review settings January 29, 2026 03:03
@coderabbitai
Copy link

coderabbitai bot commented Jan 29, 2026

Warning

Rate limit exceeded

@odilitime has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 7 minutes and 47 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

Walkthrough

Introduces optional GitHub authentication, persistent room-aware GitHub state, extensive provider refactor/additions, a PR split workflow with working-copy management, token-aware action/provider gating, new utilities (context management, shell exec, state persistence), banner output, and widespread branding/readme updates.

Changes

Cohort / File(s) Summary
Branding & Docs
​.gitignore, README.md, examples/github-webhook-agent.json, src/__tests__/*, src/__tests__/e2e.test.ts
Lowercased branding ("ElizaOS" → "elizaos"), README restructured (Quick Start, env/token guidance), small test string updates, and example JSON reformatting.
Package Manifest
package.json
Description casing changed; added @elizaos/plugin-git; removed direct ngrok dependency and moved to optional peer dependency; added webhook/server-related agentConfig parameters and relaxed GITHUB_TOKEN requirement.
Core GitHub Service
src/services/github.ts
GITHUB_TOKEN made optional; introduced GitHubAuthMode, isAuthenticated/requireAuth, mode-aware init and rate-limit handling, new error types, and many operations now enforce authentication conditionally.
State Persistence
src/utils/state-persistence.ts
New room-aware persisted state API: PersistedGitHubState, load/save/merge/clear helpers used across actions/providers.
Context Management Utilities
src/utils/context-management.ts
New token-budgeting and context utilities: mode parsing, token estimation/truncation, comment formatting, list budgeting, and provider-limit enforcement.
Shell / Working-copy Helpers
src/utils/shell-exec.ts
New secure shell exec and git wrapper, path/branch/repo sanitizers, and validation utilities for filesystem operations.
Action Layer: existing files updated
src/actions/autoCoder.ts, src/actions/branches.ts, src/actions/issues.ts, src/actions/pullRequests.ts, src/actions/repository.ts, src/actions/search.ts, src/actions/stats.ts, src/actions/webhooks.ts
Added isGitHubAuthenticated checks, state persistence (mergeAndSaveGitHubState/loadGitHubState), authentication gating for state-changing actions, improved parsing/robustness, and various output/format tweaks.
PR Split Feature (new)
src/actions/prSplit.ts
New comprehensive PR-splitting workflow: clone/analyze/suggest/refine/confirm/execute/show plan actions, analysis and plan types, AI-driven suggestion parsing, execution using working copies, and persisted plans/analysis.
Repository & Working-copy Actions (new/updated)
src/actions/repository.ts, src/providers/workingCopies.ts
New clone/list/delete working-copy actions and providers; working-copy path helpers and safety checks; state persisted for working copies.
Providers Refactor & New Providers
src/providers/github.ts, src/providers/index.ts, src/providers/*
Extracted inline providers into individual modules and added many new providers: activity, branches, issues, prComments, prSplitPlan, pullRequests, rateLimit/status, repository, user, webhooks, workingCopies, help, settings, and related helpers (getSingleComment, getFilteredComments, getWorkingCopiesBaseDir, getReposDir, getWorkingCopyPath).
PR Feedback & Aggregation
src/actions/pullRequests.ts, src/providers/prComments.ts, src/providers/prSplitPlan.ts
New actions/providers to aggregate/summarize PR feedback (comments, reviews, bots), prFeedbackCache with TTL, feedback summarization, and prSplitPlan provider integrating split plans and analysis.
Plugin Index & Public API Exports
src/index.ts
Added token requirement types/mappings, filterActionsByToken/filterProvidersByToken, exported many new utilities/providers/state helpers, integrated banner printing, and restructured action/provider composition.
Startup Banner
src/banner.ts
New banner printer exposing printGitHubBanner(runtime) that displays masked GitHub settings/status.
Tests & Examples
src/__tests__/*, examples/*
Small test string and README text casing updates; example JSON formatting/order tweaks.

Sequence Diagram

sequenceDiagram
    participant Agent as Agent Runtime
    participant Action as Action Handler
    participant GH as GitHub Service
    participant State as State Persistence
    participant Git as Local Git (working copy)
    participant LLM as Language Model

    Agent->>Action: invoke PR split or repo action
    Action->>GH: isAuthenticated? / requireAuth()
    GH-->>Action: auth status (authenticated/unauthenticated)
    alt Authenticated
        Action->>GH: fetch PR / repo data
        GH-->>Action: PR/repo response
        Action->>LLM: request analysis / split suggestions
        LLM-->>Action: JSON plan/suggestions
        Action->>State: mergeAndSaveGitHubState(plan, analysis)
        State-->>Action: persisted
        Action->>Git: clone/update working copy (gitExec)
        Git-->>Action: local repo ready
        Action->>Git: create branches / apply splits
        Git-->>Action: result
        Action->>GH: create PRs / push branches (if required)
        GH-->>Action: result
        Action-->>Agent: aggregated result
    else Unauthenticated
        Action-->>Agent: limited mode or auth-required error
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐰 I nibble code and hop with cheer,

Plans split clean, working copies near,
State saved snug in persistent ground,
Tokens watched, providers abound,
elizaos hums — a rabbit's cheer!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(plugin-github): support unauthenticated mode for public API access' is clear, concise, and directly reflects the primary feature added in this changeset: enabling the GitHub plugin to work without authentication.
Docstring Coverage ✅ Passed Docstring coverage is 95.74% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch odi-dev

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 and usage tips.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request adds unauthenticated mode support to the GitHub plugin, allowing the plugin to operate without a GitHub token for public-only operations with reduced rate limits (60 requests/hour vs 5000 requests/hour when authenticated).

Changes:

  • Added support for unauthenticated mode with automatic detection based on token presence
  • Implemented state persistence utilities for daemon restarts and cross-action state sharing
  • Added new context providers and actions for PR splitting, working copies, and enhanced feedback aggregation

Reviewed changes

Copilot reviewed 34 out of 35 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
src/services/github.ts Added authMode property, isAuthenticated() and requireAuth() methods, improved error messages for unauthenticated mode
src/utils/state-persistence.ts New utility for persisting GitHub state to cache across daemon restarts
src/utils/context-management.ts New utility for managing context token budgets and preventing provider bloat
src/providers/*.ts Split monolithic github.ts provider into separate files, added new providers for status, working copies, webhooks, etc.
src/actions/prSplit.ts New actions for analyzing and splitting large PRs into smaller focused PRs
src/actions/repository.ts Added clone, list, and delete working copy actions
src/index.ts Added token filtering logic, banner integration, dependency updates
src/banner.ts New startup banner showing GitHub configuration status
package.json Changed dependencies from direct ngrok to @elizaos/plugin-ngrok and @elizaos/plugin-git

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

const currentCommit = commitOut.trim();

// Persist to state
await mergeAndSaveGitHubState(runtime, message.roomId, {
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

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

The function signature has parameters in the wrong order. The function is defined as mergeAndSaveGitHubState(runtime, newState, roomId?) but it's being called throughout the codebase with mergeAndSaveGitHubState(runtime, roomId, newState). This will cause runtime errors where roomId is treated as the state object and newState is treated as the roomId.

Copilot uses AI. Check for mistakes.
Comment on lines 1141 to 1143
await mergeAndSaveGitHubState(runtime, message.roomId, {
lastPrSplitPlan: confirmedPlan,
});
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

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

Inconsistent parameter order for mergeAndSaveGitHubState calls. In this file, the function is called with (runtime, message.roomId, {...}) pattern (roomId before state), but in other files like issues.ts, repository.ts, and pullRequests.ts, it's called with (runtime, {...}, message.roomId) pattern (state before roomId). The function definition is mergeAndSaveGitHubState(runtime, newState, roomId?), so the calls in this file are incorrect - they pass roomId as the state parameter.

Suggested change
await mergeAndSaveGitHubState(runtime, message.roomId, {
lastPrSplitPlan: confirmedPlan,
});
await mergeAndSaveGitHubState(runtime, {
lastPrSplitPlan: confirmedPlan,
}, message.roomId);

Copilot uses AI. Check for mistakes.
const outdatedThreads = reviewComments.filter((c: any) =>
!c.in_reply_to_id && c.position === null
).length;
const activeThreads = codeThreads - outdatedThreads;
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

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

Unused variable activeThreads.

Suggested change
const activeThreads = codeThreads - outdatedThreads;

Copilot uses AI. Check for mistakes.
try {
const persistedState = await loadGitHubState(runtime, message.roomId);
const splitPlan = persistedState?.lastPrSplitPlan as PrSplitPlan | undefined;
const analysis = persistedState?.lastPrAnalysis as PrAnalysis | undefined;
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

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

Unused variable analysis.

Suggested change
const analysis = persistedState?.lastPrAnalysis as PrAnalysis | undefined;

Copilot uses AI. Check for mistakes.
truncateToTokens,
summarizeComment,
formatCommentDetail,
formatListWithBudget,
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

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

Unused import formatListWithBudget.

Suggested change
formatListWithBudget,

Copilot uses AI. Check for mistakes.
parseContextOptions,
getTokenBudget,
truncateToTokens,
formatListWithBudget,
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

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

Unused import formatListWithBudget.

Suggested change
formatListWithBudget,

Copilot uses AI. Check for mistakes.
Comment on lines +335 to +348
* Filter actions based on token availability.
*/
function filterActionsByToken(actions: Action[], hasToken: boolean): Action[] {
return actions.filter((action) => {
const requirement = actionTokenRequirements[action.name] || "enhanced";
if (requirement === "required" && !hasToken) {
logger.debug(`[GitHub] Disabling action ${action.name} - requires token`);
return false;
}
return true;
});
}

/**
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

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

Unused function filterActionsByToken.

Suggested change
* Filter actions based on token availability.
*/
function filterActionsByToken(actions: Action[], hasToken: boolean): Action[] {
return actions.filter((action) => {
const requirement = actionTokenRequirements[action.name] || "enhanced";
if (requirement === "required" && !hasToken) {
logger.debug(`[GitHub] Disabling action ${action.name} - requires token`);
return false;
}
return true;
});
}
/**

Copilot uses AI. Check for mistakes.
Comment on lines +349 to +362
* Filter providers based on token availability.
*/
function filterProvidersByToken(providers: Provider[], hasToken: boolean): Provider[] {
return providers.filter((provider) => {
const requirement = providerTokenRequirements[provider.name] || "none";
if (requirement === "required" && !hasToken) {
logger.debug(`[GitHub] Disabling provider ${provider.name} - requires token`);
return false;
}
return true;
});
}

/**
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

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

Unused function filterProvidersByToken.

Suggested change
* Filter providers based on token availability.
*/
function filterProvidersByToken(providers: Provider[], hasToken: boolean): Provider[] {
return providers.filter((provider) => {
const requirement = providerTokenRequirements[provider.name] || "none";
if (requirement === "required" && !hasToken) {
logger.debug(`[GitHub] Disabling provider ${provider.name} - requires token`);
return false;
}
return true;
});
}
/**

Copilot uses AI. Check for mistakes.
shallow,
};

const existingState = await loadGitHubState(runtime, message.roomId);
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

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

The value assigned to existingState here is unused.

Suggested change
const existingState = await loadGitHubState(runtime, message.roomId);

Copilot uses AI. Check for mistakes.
localPath: repoDir,
branch: currentBranch,
commit: currentCommit,
clonedAt: operation === "clone" ? new Date().toISOString() : (existingState?.workingCopies?.[`${owner}/${repo}`]?.clonedAt || new Date().toISOString()),
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

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

Variable 'existingState' is used before its declaration.

Copilot uses AI. Check for mistakes.
const outdatedThreads = reviewComments.filter((c: any) =>
!c.in_reply_to_id && c.position === null
).length;
const activeThreads = codeThreads - outdatedThreads;
Copy link

Choose a reason for hiding this comment

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

Unused variable calculated but never referenced

Low Severity

The activeThreads variable is calculated as codeThreads - outdatedThreads but is never used anywhere in the function. The threadInfo string and return values use discussionThreads, codeThreads, and outdatedThreads, but activeThreads is completely unreferenced. This appears to be leftover code from incomplete development - the intent may have been to show active vs outdated threads but only the calculation was added.

Fix in Cursor Fix in Web

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 18

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
package.json (1)

86-104: GITHUB_TOKEN marked as required but PR supports unauthenticated mode.

The agentConfig.pluginParameters.GITHUB_TOKEN.required is set to true, but the PR objectives state the plugin now supports unauthenticated mode for public API access. This metadata inconsistency could confuse users or tooling that relies on this configuration.

Consider updating to required: false with an updated description explaining the two modes.

Suggested fix
   "agentConfig": {
     "pluginType": "elizaos:plugin:1.0.0",
     "pluginParameters": {
       "GITHUB_TOKEN": {
         "type": "string",
-        "description": "GitHub Personal Access Token or Fine-grained token",
-        "required": true
+        "description": "GitHub Personal Access Token or Fine-grained token. Optional for read-only access to public repositories (60 requests/hour). Required for write operations and private repos (5,000 requests/hour).",
+        "required": false
       },
src/actions/autoCoder.ts (1)

219-223: Inconsistent parameter naming: max_tokens vs maxTokens.

Lines 91 and 637 use maxTokens, but this invocation and line 299 still use max_tokens. This inconsistency could cause issues if the runtime expects a specific parameter name.

Suggested fix
       const analysisResponse = await runtime.useModel(ModelType.TEXT_LARGE, {
         prompt: analysisPrompt,
         temperature: 0.2,
-        max_tokens: 1000,
+        maxTokens: 1000,
       });

And similarly at line 299:

       const codeResponse = await runtime.useModel(ModelType.TEXT_LARGE, {
         prompt: codeGenPrompt,
         temperature: 0.1,
-        max_tokens: 3000,
+        maxTokens: 3000,
       });
src/actions/webhooks.ts (1)

188-193: Inconsistent state access pattern.

Line 190 checks state.github?.lastRepository but lines 191-192 access state.data?.github?.lastRepository. This inconsistency appears in multiple handlers (lines 385-387, 516-518, 638-640) and could lead to missed fallback values.

🐛 Proposed fix for createWebhookAction
       if (!owner || !repo) {
         // Try to get from current state
-        if (state.github?.lastRepository) {
-          owner = owner || state.data?.github?.lastRepository?.owner?.login;
-          repo = repo || state.data?.github?.lastRepository?.name;
+        const lastRepo = state.data?.github?.lastRepository || state.github?.lastRepository;
+        if (lastRepo) {
+          owner = owner || lastRepo.owner?.login;
+          repo = repo || lastRepo.name;
         } else {
src/services/github.ts (1)

154-163: Headers are at the response root level, not in the data object.
Only validateAuthentication() uses the broken pattern; all other octokit calls in the file correctly destructure headers from the response root. This causes rate limits to silently fail to update here since data.headers is undefined and the fallback empty object is used instead.

🔧 Suggested fix
- const { data } = await this.octokit.users.getAuthenticated();
- this.updateRateLimit((data as any)?.headers || {});
+ const { data, headers } = await this.octokit.users.getAuthenticated();
+ this.updateRateLimit(headers);
🤖 Fix all issues with AI agents
In `@README.md`:
- Around line 109-116: The fenced code block containing the example prompts
(lines starting with "Get information about the elizaOS/eliza repository",
"Create a new repository called my-awesome-project", etc.) is missing a language
specifier; update the opening fence from ``` to ```text (or remove the fences)
so the block is annotated as plain text to satisfy MD040—modify the block that
wraps those quoted example strings accordingly.
- Around line 9-14: Update the README table to mark GITHUB_TOKEN as optional
(change `GITHUB_TOKEN` Required from **Yes** to No) and add a brief
parenthetical note in the Description column that the plugin supports
unauthenticated mode for public API access with a 60 requests/hour rate limit;
reference the existing variable name `GITHUB_TOKEN` and include a short sentence
clarifying when a token is needed (e.g., for higher rate limits or private repo
access).

In `@src/actions/issues.ts`:
- Around line 39-46: The current validation in src/actions/issues.ts uses
hasIssueUrl to only match full GitHub URLs and will miss shorthand patterns;
update the regex used for hasIssueUrl (which checks the variable text) to a
combined pattern that accepts full URLs (github.com/owner/repo/issues/123),
owner/repo with an issue marker (owner/repo#123 or owner/repo issue `#123`), and
plain "issue `#123`" when accompanied by an owner/repo mention; modify the single
test(/.../) expression for hasIssueUrl accordingly so the handler's accepted
patterns (shorthand owner/repo notation and issue number extraction) pass
validation while still rejecting unrelated text.

In `@src/actions/prSplit.ts`:
- Around line 193-208: The call to mergeAndSaveGitHubState is using the wrong
argument order (passing roomId as the second argument), so newState is being set
incorrectly; update all call sites (e.g., the call near where runtime and
message.roomId are used) to call mergeAndSaveGitHubState(runtime, newState,
roomId) instead of mergeAndSaveGitHubState(runtime, roomId, newState) —
specifically fix the call in prSplit.ts (the block creating workingCopies with
owner/repo/branch/commit/prNumber/prTitle) and the other listed sites so runtime
remains first, the new state object (containing workingCopies and
lastPullRequest) is passed second, and message.roomId (or roomId) is passed
third.
- Around line 1073-1078: The validate function currently sets hasPlanContext to
always true because of the trailing "|| true"; remove "|| true" from
hasPlanContext in the validate method so it actually checks for plan-related
keywords, and tighten the regex to reliably detect planning context (e.g.,
/\b(split|plan|pr|changes|diff|proposal)\b/i) while keeping the confirm-intent
check (hasConfirmIntent) intact so the action only triggers when both confirm
intent and a true plan context are present; ensure you reference the validate
function, hasPlanContext variable, and message.content.text when making this
change.
- Around line 994-1026: Normalize and validate parsed.updatedSuggestions before
building updatedPlan and calling mergeAndSaveGitHubState: iterate
parsed.updatedSuggestions (used to create updatedPlan in the prSplit flow),
ensure each suggestion has required fields (name:string, description:string,
priority:number, files:array, dependencies:array) and coerce or set safe
defaults (e.g., [] for files/dependencies, 999 for missing priority, empty
string for name/description); replace parsed.updatedSuggestions with the
normalized array and use that normalized array when constructing updatedPlan and
when sorting/printing so downstream rendering (Files:
suggestion.files.slice(...), sorting by priority) never encounters undefined or
non-array values. Ensure normalization happens before creating updatedPlan and
before the for-loop that generates responseText.
- Around line 568-610: The parsed LLM output may omit or malform fields, so
before constructing PrSplitPlan and before using splitPlan.suggestions you must
normalize and validate the suggestions array: ensure parsed.suggestions is an
array (default to []), map each suggestion to a safe shape with defaults for
name (""), description (""), priority (Number.MAX_SAFE_INTEGER or incremental),
files (empty array), dependencies (empty array), and coerce types (e.g., files
and dependencies to arrays, priority to number); set splitPlan.rationale =
String(parsed.rationale || ""); replace direct .length and .sort usages with the
normalized suggestions (the variable used to build splitPlan.suggestions) and
then save via mergeAndSaveGitHubState and continue formatting using the
sanitized splitPlan.suggestions to avoid runtime errors in the loop and sorting.
- Around line 141-187: The code embeds the GitHub token in clone URLs and builds
shell-interpolated commands via execAsync, risking credential leakage and
command injection; replace usages of execAsync/cloneCmd (created from exec +
promisify) with child_process.execFile or spawn that accept argument arrays and
set git config http.extraheader for authentication (use a Base64 of
"x-access-token:<token>" as the Authorization header) when token present; update
the paths/branches operations (where alreadyCloned, headRepo, headBranch,
repoDir, getWorkingCopyPath are used) to pass branch and repo path as arguments
instead of interpolating into command strings and run commands with cwd set to
repoDir so no credentials end up in .git/config or shell history.
- Around line 762-779: The git commands currently build shell strings with
untrusted model output (split.files and split.description) in execAsync;
validate that split.files is a non-empty array and sanitize/escape or — better —
switch execAsync usage to an API that accepts argv (e.g., spawn/execFile) so you
pass file paths and commit message as separate arguments instead of
interpolating into a shell, and ensure you also pass analysis.headBranch and
branchName as argv rather than concatenating; also handle/normalize/validate
each filename (no path traversal, no shell metacharacters) before using it and
construct git add/checkout/commit arguments as arrays to avoid shell
interpretation.

In `@src/actions/repository.ts`:
- Around line 867-881: The code uses existingState inside the workingCopyData
object before existingState is declared, causing a TDZ/ReferenceError; fix by
moving the await loadGitHubState(runtime, message.roomId) call (the
existingState declaration) to before you build workingCopyData, then construct
workingCopyData (which references existingState) and finally call
mergeAndSaveGitHubState as before; ensure you keep the same variable names
(existingState, workingCopyData) and preserve the ternary that reads
existingState?.workingCopies?.[`${owner}/${repo}`]?.clonedAt.
- Around line 751-785: Validate and sanitize the untrusted owner and repo before
using them to build repoDir: enforce a strict regex (e.g., allow only
lowercase/uppercase letters, digits, hyphen, underscore, dot) for owner and repo
and throw if they contain disallowed characters or are empty; then construct the
target path by resolving via path.resolve(getReposDir(), owner, repo) (or
resolve(getReposDir(), ...owner.split(path.sep), repo) after stripping leading
slashes) and verify the resolved repoDir startsWith the resolved getReposDir()
(use path.sep normalization or compare path.resolve(getReposDir()) as prefix) to
prevent path traversal; if the check fails, throw an error and do not call
getWorkingCopyPath()/fs.rm/other filesystem ops. Reference symbols: owner, repo,
getReposDir, getWorkingCopyPath, repoDir, fs.rm, path.resolve, path.sep.
- Around line 771-848: The clone/pull logic currently uses execAsync
(promisified exec) with shell-interpolated command strings exposing shell
injection via user-controlled owner/repo/branch; replace the child_process
import and promisified exec with execFile (or promisify(execFile)) and call git
as an executable with argv arrays for commands used in this file (replace uses
in cloneCmd/fallbackCmd, git fetch/checkout/pull/reset) so user values are
passed as args not interpolated into a shell, and ensure you pass cwd in the
execFile options; also move the existingState declaration to before the creation
of workingCopyData so workingCopyData does not reference existingState before
it’s defined (look for symbols execAsync/cloneCmd/fallbackCmd and
workingCopyData/existingState).

In `@src/banner.ts`:
- Around line 73-96: The banner currently always marks GITHUB_TOKEN as required;
change the settings array construction (the settings constant that includes the
GITHUB_TOKEN entry) so the required flag for GITHUB_TOKEN is computed from the
app's auth mode instead of hard-coded—e.g., compute a boolean (like
githubTokenRequired) from your existing auth/public-only indicator (use your
existing isPublicOnly / isUnauthenticated flag or function) and set { name:
'GITHUB_TOKEN', value: token as string, isSecret: true, required:
githubTokenRequired }; leave other entries (GITHUB_OWNER, GITHUB_WEBHOOK_SECRET)
and the use of formatSettingValue untouched so the banner reflects the correct
required status.

In `@src/providers/activity.ts`:
- Around line 92-111: The rate limit usage calculation in the activity provider
can divide by zero (when computing usage = Math.round((rateLimit.used /
rateLimit.limit) * 100)); update the block that reads githubState.rateLimit to
guard against rateLimit.limit === 0 by computing usage only when limit > 0
(e.g., set usage = 0 or an appropriate sentinel when limit is 0), and ensure
values.rateLimitUsage and any contextText string interpolation use this safe
usage value; look for identifiers rateLimit, usage, githubState,
values.rateLimitUsage, contextText, and data.rateLimit to apply the change.

In `@src/providers/rateLimit.ts`:
- Around line 27-29: Guard against division-by-zero when computing usedPercent
from githubService.getRateLimit(): check rateLimit.limit (from the rateLimit
object) and if it's missing or <= 0, set usedPercent to 0 (or a safe fallback)
instead of performing the division; otherwise compute usedPercent as
Math.round((rateLimit.used / rateLimit.limit) * 100). Also ensure any downstream
consumer of usedPercent handles the fallback consistently. Reference: rateLimit,
usedPercent, resetTime, and githubService.getRateLimit().

In `@src/services/github.ts`:
- Around line 65-67: checkRateLimit and getRateLimitStatus assume a fixed 5000
limit and use a hardcoded "< 100" threshold, which breaks unauthenticated flows
(octokit with rateLimitRemaining default 60). Update both methods to read the
actual x-ratelimit-limit header from Octokit responses and store it (introduce
or update a rateLimitLimit property if needed), compute the threshold
dynamically (e.g., threshold = Math.min(100, Math.floor(rateLimitLimit * 0.02))
or similar) and compute used as rateLimitLimit - rateLimitRemaining; ensure
checkRateLimit uses rateLimitLimit and rateLimitRemaining/rateLimitReset values
to decide waiting and that getRateLimitStatus returns accurate limit, remaining,
used, and reset based on those tracked headers rather than hardcoded values
(modify functions checkRateLimit and getRateLimitStatus and any response-parsing
logic where headers are read).

In `@src/utils/context-management.ts`:
- Around line 56-63: The parser's regex maps the token "full" to "detail",
making the ContextMode value "full" unreachable; update the detection logic in
the block that sets options.mode (using variable lower and options.mode) so that
"full" is matched to options.mode = "full" (e.g., separate the full/all/verbose
pattern from the detail pattern) while leaving minimal and summary branches
intact; ensure the regexes for detail (e.g., "detailed", "details") do not
include "full" so ContextMode can be set to "full" where appropriate.

In `@src/utils/state-persistence.ts`:
- Around line 196-226: mergeAndSaveGitHubState currently deep-merges
repositories/issues/pullRequests but overwrites other map-like fields causing
workingCopies and prFeedbackCache to be lost; update mergedState inside
mergeAndSaveGitHubState to deep-merge workingCopies and prFeedbackCache (e.g., {
...existing.workingCopies, ...newState.workingCopies }) and ensure sensible
defaults (empty objects) so existing entries are preserved while new entries
override.
🧹 Nitpick comments (11)
src/providers/webhooks.ts (1)

58-60: Include error details in debug log.
Helps diagnose state-load failures without changing behavior.

Proposed diff
-    } catch (error) {
-      logger.debug("[GitHub] Could not load webhooks context");
+    } catch (error) {
+      logger.debug({ error }, "[GitHub] Could not load webhooks context");
       return { text: "", values: {}, data: {} };
     }
src/providers/branches.ts (1)

70-72: Include the caught error in the debug log.
It makes troubleshooting persistence failures easier.

🛠️ Proposed tweak
-      logger.debug("[GitHub] Could not load branch protection context");
+      logger.debug({ error }, "[GitHub] Could not load branch protection context");
src/providers/prSplitPlan.ts (1)

33-41: Prefer in-memory state before loading persistence.
This avoids stale context and unnecessary cache reads when runtime state already has the plan.

♻️ Proposed update
-        _state: State | undefined,
+        state: State | undefined,
@@
-            const persistedState = await loadGitHubState(runtime, message.roomId);
-            const splitPlan = persistedState?.lastPrSplitPlan;
-            const analysis = persistedState?.lastPrAnalysis;
+            let githubState = state?.github;
+            if (!githubState) {
+                githubState = await loadGitHubState(runtime, message.roomId);
+            }
+            const splitPlan = githubState?.lastPrSplitPlan;
+            const analysis = githubState?.lastPrAnalysis;
src/providers/user.ts (1)

22-34: Short-circuit when unauthenticated to avoid noisy errors.
In unauthenticated mode this will throw every time; an early return keeps logs clean.

🔧 Proposed guard
       const githubService = runtime.getService<GitHubService>("github");
 
       if (!githubService) {
         return {
           text: "",
           values: {},
           data: {},
         };
       }
+
+      if (!githubService.isAuthenticated()) {
+        return {
+          text: "GitHub user information unavailable (unauthenticated)",
+          values: {},
+          data: {},
+        };
+      }
src/providers/repository.ts (2)

47-68: Consider defensive checks for nested object properties.

When accessing repo.owner.login on line 59, there's no null check for repo.owner. If lastRepository exists but has a malformed structure (e.g., missing owner), this would throw a runtime error.

🛡️ Suggested defensive access
         values.currentRepository = repo.full_name;
-        values.repositoryOwner = repo.owner.login;
+        values.repositoryOwner = repo.owner?.login;
         values.repositoryName = repo.name;

74-92: Type assertion may hide runtime issues.

The cast as GitHubRepository[] on line 76 assumes all values in githubState.repositories conform to GitHubRepository. If the persisted state contains malformed or partial data, this could cause runtime errors when accessing properties like updated_at or full_name.

Consider adding a filter or validation step to ensure data integrity.

src/actions/issues.ts (2)

19-25: Duplicated helper function - consider importing from shared location.

The isGitHubAuthenticated function is defined locally here, but an identical version exists in src/index.ts (lines 365-368) and is exported. This duplication creates maintenance burden if the logic needs to change.

♻️ Proposed fix
 import {
   GitHubService,
   type GitHubIssue,
   type CreateIssueOptions,
+  isGitHubAuthenticated,
 } from "../index";
 import { mergeAndSaveGitHubState } from "../utils/state-persistence";

-/**
- * Check if the GitHub service is available and authenticated.
- */
-function isGitHubAuthenticated(runtime: IAgentRuntime): boolean {
-  const githubService = runtime.getService<GitHubService>("github");
-  return githubService?.isAuthenticated() ?? false;
-}

670-674: Minor formatting inconsistency.

Line 672 has inconsistent indentation compared to surrounding code.

🧹 Proposed formatting fix
           const repoName = issue.html_url
             ? issue.html_url.match(/github\.com\/([^\/]+\/[^\/]+)/)?.[1] ||
-            "unknown"
+              "unknown"
             : "unknown";
src/actions/webhooks.ts (1)

13-19: Duplicated helper function - same issue as in issues.ts.

The isGitHubAuthenticated function is duplicated here and in src/actions/issues.ts. Import from src/index.ts instead.

♻️ Proposed fix
 import { GitHubService } from "../services/github";
+import { isGitHubAuthenticated } from "../index";
 import { z } from "zod";

-/**
- * Check if the GitHub service is available and authenticated.
- */
-function isGitHubAuthenticated(runtime: IAgentRuntime): boolean {
-  const githubService = runtime.getService<GitHubService>("github");
-  return githubService?.isAuthenticated() ?? false;
-}
src/providers/workingCopies.ts (1)

92-111: Sequential async operations could be parallelized.

The loop awaits fs.stat sequentially for each working copy. With many working copies, this could be slow. Consider using Promise.all for parallel execution.

⚡ Proposed optimization
-      for (const [key, data] of entries) {
-        let exists = false;
-        try {
-          const stat = await fs.stat(path.join(data.localPath, ".git"));
-          exists = stat.isDirectory();
-        } catch {
-          exists = false;
-        }
-
-        workingCopyList.push({
-          key,
-          owner: data.owner,
-          repo: data.repo,
-          localPath: data.localPath,
-          branch: data.branch,
-          commit: data.commit,
-          exists,
-          clonedAt: data.clonedAt,
-        });
-      }
+      const workingCopyList = await Promise.all(
+        entries.map(async ([key, data]) => {
+          let exists = false;
+          try {
+            const stat = await fs.stat(path.join(data.localPath, ".git"));
+            exists = stat.isDirectory();
+          } catch {
+            exists = false;
+          }
+          return {
+            key,
+            owner: data.owner,
+            repo: data.repo,
+            localPath: data.localPath,
+            branch: data.branch,
+            commit: data.commit,
+            exists,
+            clonedAt: data.clonedAt,
+          };
+        })
+      );
src/actions/pullRequests.ts (1)

1083-1122: Cap feedback payload before LLM summarization.

allFeedback.join() can get huge on busy PRs and exceed model context. Consider truncating using the new context utilities before building the prompt.

🧩 Example using context-management utilities
+import { truncateToTokens, getTokenBudget } from "../utils/context-management";
...
-      const summaryPrompt = `Analyze and summarize the feedback on this pull request:
+      const feedbackText = truncateToTokens(
+        allFeedback.join("\n\n"),
+        getTokenBudget({ mode: "detail", maxTokens: 8000 })
+      );
+      const summaryPrompt = `Analyze and summarize the feedback on this pull request:
...
-**Feedback to analyze:**
-${allFeedback.join("\n\n")}
+**Feedback to analyze:**
+${feedbackText}

odilitime and others added 9 commits January 30, 2026 01:26
**Breaking:** Ngrok is now an optional peer dependency instead of required

**Performance Improvements:**
- Remove 5+ redundant LLM calls per webhook command (95% reduction)
- Replace LLM-based intent analysis with simple keyword matching
- Validation now uses pattern matching instead of TEXT_LARGE model calls

**Webhook URL Detection (Progressive Enhancement):**
1. GITHUB_WEBHOOK_URL - Explicit override (highest priority)
2. PUBLIC_URL - Auto-constructs webhook endpoint
3. SERVER_HOST + SERVER_PORT - Hostname-based URL construction
4. Ngrok tunnel - Fallback for local development (lowest priority)

**Configuration:**
- Added GITHUB_WEBHOOK_URL, PUBLIC_URL, SERVER_HOST, SERVER_PORT, SERVER_PROTOCOL settings
- Ngrok now auto-detected when available, not required
- Works on any public hostname/IP without tunnel services

**Impact:**
- Production-ready for cloud deployments (Railway, Render, Vercel, K8s, etc.)
- Faster webhook operations (no LLM roundtrips)
- Lower API costs (~95% reduction for webhook commands)
- Better developer experience with clear configuration priority

**Files Changed:**
- src/actions/webhooks.ts: Refactored all 4 webhook actions
- package.json: Moved ngrok to optional peerDependency
- README.md: Added comprehensive webhook configuration docs

Co-authored-by: Cursor <cursoragent@cursor.com>
**Documentation:**
- Mark GITHUB_TOKEN as optional with unauthenticated mode note (60 req/hr)
- Add language specifier to markdown code block (fixes MD040 lint)

**Issue Action:**
- Update validation regex to accept shorthand patterns (owner/repo#123, issue #123)
- Supports full URLs, shorthand notation, and plain issue numbers with context

**PR Split Actions:**
- Fix mergeAndSaveGitHubState argument order (newState, roomId) across all calls
- Remove || true from hasPlanContext validation to properly check planning keywords
- Add normalizeSuggestions helper to sanitize LLM responses
- Normalize parsed.suggestions and parsed.updatedSuggestions before use
- Prevent undefined/non-array values in files, dependencies, priority fields
- Set safe defaults: [] for arrays, incremental priority, empty strings for text

**Impact:**
- Prevents runtime errors from malformed LLM responses
- Fixes state persistence bugs (workingCopies, plans now saved correctly)
- Improves action validation accuracy
- Better support for natural language issue references

Co-authored-by: Cursor <cursoragent@cursor.com>
**New Utility: shell-exec.ts**
- Created centralized secure shell execution module
- Uses execFile (no shell interpolation) to prevent command injection
- Provides gitExec wrapper for common git operations
- Input sanitization functions:
  - sanitizeRepoIdentifier: Validates owner/repo names
  - sanitizeBranchName: Validates git branch names
  - sanitizeFilePaths: Validates file path arrays
  - validatePath: Prevents path traversal attacks

**Security Improvements in prSplit.ts:**
- Replaced all execAsync (shell interpolation) with gitExec
- Sanitize all user inputs (owner, repo, branch, files)
- Use git config http.extraheader for authentication (no tokens in URLs)
- Validate paths against base directory to prevent traversal
- Pass arguments as arrays instead of shell strings

**Examples:**
Before: execAsync(`git clone ${url} "${dir}"`) // Shell injection risk
After: gitExec(['clone', url, dir]) // Safe argument passing

**Impact:**
- Eliminates command injection vulnerabilities
- Prevents credential leakage in git URLs/history
- Blocks path traversal attacks
- Maintains functionality while improving security posture

Co-authored-by: Cursor <cursoragent@cursor.com>
**Bug Fixes:**
- Fix TDZ error: Load existingState BEFORE using it in workingCopyData
- Move loadGitHubState call before workingCopyData construction

**Security Improvements:**
- Replace all execAsync with gitExec (no shell interpolation)
- Sanitize owner/repo inputs with sanitizeRepoIdentifier
- Sanitize branch names with sanitizeBranchName
- Validate paths with validatePath to prevent traversal
- Use git config http.extraheader for auth (no tokens in URLs)

**Changes Applied:**
- cloneGitHubRepositoryAction: Secure clone/pull operations
- listWorkingCopiesAction: Secure git status checks
- deleteWorkingCopyAction: Secure safety checks

**Impact:**
- Fixes runtime error from using existingState before declaration
- Eliminates command injection in all repository operations
- Prevents path traversal attacks
- Keeps tokens out of git history and shell history

Co-authored-by: Cursor <cursoragent@cursor.com>
**Banner (banner.ts):**
- Make GITHUB_TOKEN required flag dynamic based on auth status
- Shows as required only when not authenticated

**Rate Limit Handling (github.ts):**
- Add rateLimitLimit property to track actual API limit
- Initialize correctly for auth (5000) vs unauth (60) modes
- Read x-ratelimit-limit header from API responses
- Use dynamic threshold (2% of limit, min 10) instead of hardcoded 100
- Fix getRateLimitStatus to use actual limit instead of hardcoded 5000

**Division by Zero Guards:**
- activity.ts: Guard usage calculation when limit is 0
- rateLimit.ts: Guard usedPercent calculation when limit is 0
- Prevents NaN/Infinity in rate limit display

**Context Mode Detection (context-management.ts):**
- Separate "full" pattern from "detail" pattern
- Fix regex order so "full" maps to ContextMode.full correctly
- Pattern priority: minimal > summary > full > detail

**State Persistence (state-persistence.ts):**
- Deep merge workingCopies to preserve existing clones
- Deep merge prFeedbackCache to preserve cached PR data
- Prevents data loss when updating state with new entries

**Impact:**
- Accurate rate limit tracking for both auth modes
- No more NaN in rate limit displays
- Working "full" context mode
- State updates preserve existing data correctly
- Dynamic banner based on actual auth status

Co-authored-by: Cursor <cursoragent@cursor.com>
**package.json:**
- Mark GITHUB_TOKEN as optional (required: false)
- Update description to explain unauthenticated mode support
- Clarifies 60 req/hr public access vs 5000 req/hr with token

**autoCoder.ts:**
- Standardize parameter naming: max_tokens → maxTokens
- Consistent with useModel API across all calls (lines 222, 299)

**webhooks.ts:**
- Fix inconsistent state access pattern
- Use state?.data?.github consistently across all handlers
- Add proper null checking before accessing lastRepository
- Fixes createWebhook, listWebhooks, deleteWebhook, pingWebhook

**github.ts:**
- Fix validateAuthentication headers destructuring bug
- Headers are at response root, not in data object
- Change: const { data } → const { data, headers }
- Ensures rate limits update correctly during auth validation

**Impact:**
- Correct metadata for unauthenticated mode support
- Prevents runtime errors from malformed state access
- Fixes rate limit tracking during authentication
- Consistent API parameter naming

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🤖 Fix all issues with AI agents
In `@src/actions/issues.ts`:
- Around line 415-422: The validate function currently returns
isGitHubAuthenticated(runtime) unconditionally; change it to require both
authentication and explicit user intent to create an issue by adding an intent
check (e.g., call an existing intent helper like isCreateIssueIntent(message) or
detect the intent from the Memory/message content) and return true only when
isGitHubAuthenticated(runtime) && isCreateIssueIntent(message). Update the
validate signature in src/actions/issues.ts (the validate method for this
action) to run both checks and ensure any helper used
(isCreateIssueIntent/detectIntent) is imported or implemented and returns a
boolean.

In `@src/actions/prSplit.ts`:
- Around line 830-831: The commit currently uses split.description directly in
the gitExec(['commit', '-m', split.description]) call which will fail when
split.description is an empty string; update the commit step in
src/actions/prSplit.ts to compute a safe message (e.g., const message =
split.description && split.description.trim().length ? split.description :
'chore: apply split changes') and pass that message to gitExec(['commit', '-m',
message], { cwd: repoDir }) so commits never receive an empty -m value; ensure
you reference the split variable and the gitExec(['commit'...]) invocation when
making the change.

In `@src/actions/repository.ts`:
- Around line 361-368: The current validate function on the repository action
returns true for any authenticated user (validate in src/actions/repository.ts
uses isGitHubAuthenticated(runtime)), which is too permissive for a write
operation; update validate to require both authentication and explicit intent by
inspecting the incoming Memory (message) content/text for intent keywords (e.g.,
"create", "new", "init", "create repository", "create repo", "init repo", "make
repo", "repository") before returning true, so only when
isGitHubAuthenticated(runtime) is true AND the message contains one of those
intent tokens should validate return true; implement the keyword check safely
(case-insensitive, trim) and keep State and IAgentRuntime parameters unchanged.
- Around line 212-219: The validate function currently returns true for any
authenticated user; tighten it by adding a simple intent check on the incoming
Memory message before allowing the action. In the validate implementation in
repository.ts (the validate async function that calls
isGitHubAuthenticated(runtime)), add a small intent-matching step that inspects
the message text/content (e.g., message.text or message.content) with a
case-insensitive regex for keywords like "list", "show", "repos", or
"repositories" (for example
/(list|show).*(repo|repository|repos)|\b(repo|repository|repos)\b/i) and return
isGitHubAuthenticated(runtime) && intentMatches; keep the check lightweight and
inline (or add a small helper like matchesListReposIntent) so only authenticated
requests that match the list-repos intent pass validation.
- Around line 821-902: Summary: git config writes persist tokens to disk; use
per-command -c to avoid storing credentials. Replace all gitExec calls that run
'config' to set http.https://github.com/.extraheader (both local and global)
with adding a per-command git config flag: prepend '-c',
`http.https://github.com/.extraheader=Authorization: Basic
${Buffer.from(\`x-access-token:${token}\`).toString('base64')}` to the gitExec
argument arrays when hasToken is true (e.g., for fetch/checkout/pull/reset in
the pull branch path and for clone/fallbackArgs in the clone branch path),
remove the corresponding gitExec(['config', ...]) and any --unset calls, and
ensure cwd and other options are preserved when you call gitExec with the
modified argument arrays (refer to gitExec, hasToken, token, cloneArgs,
fallbackArgs, repoDir, branch).

In `@src/actions/webhooks.ts`:
- Around line 38-42: The current regex in src/actions/webhooks.ts (variable
webhookMatch) is too broad because it falls back to matching any "#123" (which
can be issues/PRs); change the pattern to only match webhooks by replacing
/webhook\s*#?(\d+)|#(\d+)/i with a single more specific pattern like
/webhook\s*#?(\d+)/i, then update the parsing to use the single capture group
(use parseInt(webhookMatch[1]) and assign to result.webhookId) so only explicit
"webhook" references produce a webhookId.
- Around line 32-36: The current repo extraction using
text.match(/\b([a-zA-Z0-9_-]+)\/([a-zA-Z0-9_-]+)\b/) (producing repoMatch) fails
for identifiers with dots and because \b breaks on dots; update the regex used
in the text.match call to allow dots in owner/repo (e.g. use
/([A-Za-z0-9._-]+)\/([A-Za-z0-9._-]+)/) or a non-word-boundary aware pattern (or
add appropriate lookarounds) so repoMatch[1]/repoMatch[2] correctly capture
names like user.name/repo.js.

In `@src/utils/shell-exec.ts`:
- Around line 198-207: The branch-name validation currently uses the
character-class invalidChars which erroneously treats '.' as forbidden
(rejecting single dots) because '..' cannot be expressed inside []; update the
validation in the branch name check (the invalidChars / branchName test in src
utils shell-exec.ts) to allow single dots but still disallow the two-character
sequence '..' and the other forbidden tokens; implement this by making
invalidChars only forbid whitespace and the single-character tokens (e.g., use a
character-class that includes @ { \\ ^ ~ : ? * [ but not .) and add a separate
test to explicitly reject the literal sequence '..' (or use a combined regex
with an alternation like \.\. to detect two dots); keep the thrown Error message
the same but ensure '.' is permitted in branch names.
- Around line 86-105: The catch block may set exitCode to a string (Node system
error codes like 'ENOENT'), breaking the expected number | null type; change the
exitCode computation to coerce/normalize to a numeric or null value by using a
numeric-first check: if error.code is a number use it, else if error.exitCode is
a number use that, otherwise set exitCode to null (or a numeric fallback if
desired). Update the local variable (exitCode) used in logger.warn and the
returned object so it always matches number | null; adjust references to
exitCode, stdout, stderr, logger.warn, and the returned object in the catch
block accordingly.
🧹 Nitpick comments (8)
src/actions/webhooks.ts (4)

8-11: Unused imports detected.

ModelType and z (zod) are imported but never used in this file. These appear to be leftovers from the previous LLM-based validation flow.

🧹 Proposed cleanup
 import {
   type Action,
   type IAgentRuntime,
   type Memory,
   type State,
   type HandlerCallback,
   logger,
-  ModelType,
 } from "@elizaos/core";
 import { GitHubService } from "../services/github";
-import { z } from "zod";

13-19: Duplicate helper function.

This isGitHubAuthenticated function duplicates the exported version in src/index.ts. Consider importing it from there to maintain a single source of truth.

♻️ Proposed refactor
-import { GitHubService } from "../services/github";
+import { GitHubService } from "../services/github";
+import { isGitHubAuthenticated } from "../index";

Then remove lines 13-19 (the local function definition).


80-86: Type safety lost with as any cast.

Casting tunnelService to any loses type safety. Consider defining and using a TunnelService interface for better type checking.

💡 Suggestion
interface TunnelService {
  isActive(): boolean;
  getUrl(): Promise<string>;
}

const tunnelService = runtime.getService("tunnel") as TunnelService | undefined;

589-591: Consider verifying webhook exists before pinging.

Unlike deleteWebhookAction (lines 467-479), this action doesn't verify the webhook exists before attempting to ping it. This could result in cryptic API errors. Consider adding a verification step for consistency and better error messages.

💡 Suggestion
+      // Verify webhook exists first
+      const webhooks = await githubService.listWebhooks(owner, repo);
+      const webhook = webhooks.find((w) => w.id === webhookId);
+
+      if (!webhook) {
+        await callback({
+          text: `❌ Webhook #${webhookId} not found in ${owner}/${repo}.\n\nAvailable webhooks: ${webhooks.map((w) => `#${w.id}`).join(", ") || "None"}`,
+          thought: `Webhook ${webhookId} not found`,
+        });
+        return;
+      }
+
       // Ping the webhook
       await githubService.pingWebhook(owner, repo, webhookId);
src/utils/shell-exec.ts (2)

138-140: Move path import to module top level.

Inline require inside a function is unconventional and incurs repeated module resolution overhead. Import at the top of the file with other imports.

♻️ Proposed refactor

Add to imports at top of file:

import path from "path";

Then update the function:

 export function validatePath(basePath: string, targetPath: string): string {
-    const path = require('path');
-
     // Resolve to absolute paths

236-239: Consider cross-platform absolute path detection.

The check file.startsWith('/') only catches Unix-style absolute paths. Windows absolute paths (e.g., C:\) would pass through. If cross-platform support is needed, consider using path.isAbsolute().

♻️ Proposed refactor for cross-platform support
+import path from "path";
+
 // Check for path traversal attempts
-if (file.includes('..') || file.startsWith('/')) {
+if (file.includes('..') || path.isAbsolute(file)) {
     throw new Error(`Path traversal detected in file: ${file}`);
 }
src/actions/autoCoder.ts (1)

13-19: Consider reusing the shared isGitHubAuthenticated helper.

There’s already an exported helper in src/index.ts; centralizing avoids drift. If import cycles are a concern, move it to a small shared util module.

src/services/github.ts (1)

195-214: Track rate limits separately per resource (search, core) using x-ratelimit-resource header.

GitHub's rate limits are resource-scoped: search (30/hour), core (5000/hour for authenticated), code_search (10/minute), and graphql are separate buckets with independent limits and reset times. The current implementation blindly updates global rateLimitRemaining/Limit/Reset from any response, mixing these buckets.

Your service calls both search.issuesAndPullRequests() and search.repos() along with core endpoints. When a search request leaves only 29 requests remaining (out of 30), the global counter becomes 29, then unnecessarily throttles subsequent core API calls despite having the full 5000/hour core quota. Each bucket also has its own reset time, making the single rateLimitReset incorrect.

Update updateRateLimit() to read the x-ratelimit-resource header and maintain separate counters per resource. Route checkRateLimit() checks by resource type (or call it with the appropriate resource before making search vs. core calls).

Comment on lines 415 to 422
validate: async (
runtime: IAgentRuntime,
message: Memory,
state: State | undefined,
): Promise<boolean> => {
const githubService = runtime.getService<GitHubService>("github");
return !!githubService;
// This action requires authentication since it creates an issue
return isGitHubAuthenticated(runtime);
},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate should still check user intent.
Returning true for any message when authenticated can trigger unintended issue creation. Add an intent check alongside auth.

✅ Add intent gating
-    // This action requires authentication since it creates an issue
-    return isGitHubAuthenticated(runtime);
+    if (!isGitHubAuthenticated(runtime)) return false;
+    const text = message.content?.text?.toLowerCase() || "";
+    return /create|submit|file|report|open/.test(text) && /issue|bug/.test(text);
🤖 Prompt for AI Agents
In `@src/actions/issues.ts` around lines 415 - 422, The validate function
currently returns isGitHubAuthenticated(runtime) unconditionally; change it to
require both authentication and explicit user intent to create an issue by
adding an intent check (e.g., call an existing intent helper like
isCreateIssueIntent(message) or detect the intent from the Memory/message
content) and return true only when isGitHubAuthenticated(runtime) &&
isCreateIssueIntent(message). Update the validate signature in
src/actions/issues.ts (the validate method for this action) to run both checks
and ensure any helper used (isCreateIssueIntent/detectIntent) is imported or
implemented and returns a boolean.

Comment on lines +830 to +831
await gitExec(['add', '.'], { cwd: repoDir });
await gitExec(['commit', '-m', split.description], { cwd: repoDir });
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, check if the file exists and read the context around lines 830-831
wc -l src/actions/prSplit.ts

Repository: elizaos-plugins/plugin-github

Length of output: 100


🏁 Script executed:

# Read lines around 830-831 to verify the code
sed -n '820,840p' src/actions/prSplit.ts

Repository: elizaos-plugins/plugin-github

Length of output: 1238


🏁 Script executed:

# Search for the Split type definition to understand the description field
rg -n "interface Split|type Split" src/

Repository: elizaos-plugins/plugin-github

Length of output: 126


🏁 Script executed:

# Read the SplitSuggestion interface definition
sed -n '88,120p' src/actions/prSplit.ts

Repository: elizaos-plugins/plugin-github

Length of output: 1224


🏁 Script executed:

# Search for where split.description is assigned or validated
rg -n "description\s*[=:]|split\.description" src/actions/prSplit.ts | head -20

Repository: elizaos-plugins/plugin-github

Length of output: 927


🏁 Script executed:

# Search for where split.description might be set or come from
rg -n "split\.description\s*=" src/actions/prSplit.ts

Repository: elizaos-plugins/plugin-github

Length of output: 55


🏁 Script executed:

# Look at the context around line 46 to understand the description assignment
sed -n '40,55p' src/actions/prSplit.ts

Repository: elizaos-plugins/plugin-github

Length of output: 605


🏁 Script executed:

# Search for where suggestions are created and populated
rg -n "suggestions\s*=" src/actions/prSplit.ts | head -10

Repository: elizaos-plugins/plugin-github

Length of output: 55


🏁 Script executed:

# Check the broader context around the commit to understand error handling
sed -n '800,835p' src/actions/prSplit.ts

Repository: elizaos-plugins/plugin-github

Length of output: 1947


🏁 Script executed:

# Search for how suggestions are created from AI output
rg -n "SplitSuggestion\|suggestions\s*:" src/actions/prSplit.ts | grep -A 5 "suggestions"

Repository: elizaos-plugins/plugin-github

Length of output: 55


🏁 Script executed:

# Look for the actual AI parsing that creates suggestions
sed -n '500,600p' src/actions/prSplit.ts

Repository: elizaos-plugins/plugin-github

Length of output: 3983


🏁 Script executed:

# Continue reading the AI response parsing to see how descriptions are handled
sed -n '580,630p' src/actions/prSplit.ts

Repository: elizaos-plugins/plugin-github

Length of output: 2186


🏁 Script executed:

# Check if there's any validation of split.description before the commit
sed -n '750,835p' src/actions/prSplit.ts

Repository: elizaos-plugins/plugin-github

Length of output: 4149


🏁 Script executed:

# Check if there's any validation of split.description between normalization and commit
sed -n '40,100p' src/actions/prSplit.ts

Repository: elizaos-plugins/plugin-github

Length of output: 1651


🏁 Script executed:

# Verify there's no validation between normalizeSuggestions and the commit line
rg -n "split\.description" src/actions/prSplit.ts

Repository: elizaos-plugins/plugin-github

Length of output: 166


Add a safe commit-message fallback for empty descriptions.

The commit at line 831 uses split.description directly, which can be an empty string if the LLM-generated suggestion lacks a description. Git commit with an empty -m flag will fail. Provide a fallback message:

Suggested fix
-                await gitExec(['commit', '-m', split.description], { cwd: repoDir });
+                const commitMsg = split.description?.trim() || `Split ${split.name}`;
+                await gitExec(['commit', '-m', commitMsg], { cwd: repoDir });
🤖 Prompt for AI Agents
In `@src/actions/prSplit.ts` around lines 830 - 831, The commit currently uses
split.description directly in the gitExec(['commit', '-m', split.description])
call which will fail when split.description is an empty string; update the
commit step in src/actions/prSplit.ts to compute a safe message (e.g., const
message = split.description && split.description.trim().length ?
split.description : 'chore: apply split changes') and pass that message to
gitExec(['commit', '-m', message], { cwd: repoDir }) so commits never receive an
empty -m value; ensure you reference the split variable and the
gitExec(['commit'...]) invocation when making the change.

Comment on lines 212 to 219
validate: async (
runtime: IAgentRuntime,
message: Memory,
state: State | undefined,
): Promise<boolean> => {
const githubService = runtime.getService<GitHubService>("github");
return !!githubService;
// This action requires authentication since it lists the authenticated user's repos
return isGitHubAuthenticated(runtime);
},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

List validation should still check intent.
Right now any message becomes eligible when authenticated. Add a simple intent check.

✅ Add intent gating
-    // This action requires authentication since it lists the authenticated user's repos
-    return isGitHubAuthenticated(runtime);
+    if (!isGitHubAuthenticated(runtime)) return false;
+    const text = message.content?.text?.toLowerCase() || "";
+    return /list|show|my|repositories|repos/.test(text);
🤖 Prompt for AI Agents
In `@src/actions/repository.ts` around lines 212 - 219, The validate function
currently returns true for any authenticated user; tighten it by adding a simple
intent check on the incoming Memory message before allowing the action. In the
validate implementation in repository.ts (the validate async function that calls
isGitHubAuthenticated(runtime)), add a small intent-matching step that inspects
the message text/content (e.g., message.text or message.content) with a
case-insensitive regex for keywords like "list", "show", "repos", or
"repositories" (for example
/(list|show).*(repo|repository|repos)|\b(repo|repository|repos)\b/i) and return
isGitHubAuthenticated(runtime) && intentMatches; keep the check lightweight and
inline (or add a small helper like matchesListReposIntent) so only authenticated
requests that match the list-repos intent pass validation.

Comment on lines 361 to 368
validate: async (
runtime: IAgentRuntime,
message: Memory,
state: State | undefined,
): Promise<boolean> => {
const githubService = runtime.getService<GitHubService>("github");
return !!githubService;
// This action requires authentication since it creates a repository
return isGitHubAuthenticated(runtime);
},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Create validation is too permissive for a write action.
When authenticated, this action is always eligible. Require intent keywords.

✅ Add intent gating
-    // This action requires authentication since it creates a repository
-    return isGitHubAuthenticated(runtime);
+    if (!isGitHubAuthenticated(runtime)) return false;
+    const text = message.content?.text?.toLowerCase() || "";
+    return /create|make|new/.test(text) && /repo|repository/.test(text);
🤖 Prompt for AI Agents
In `@src/actions/repository.ts` around lines 361 - 368, The current validate
function on the repository action returns true for any authenticated user
(validate in src/actions/repository.ts uses isGitHubAuthenticated(runtime)),
which is too permissive for a write operation; update validate to require both
authentication and explicit intent by inspecting the incoming Memory (message)
content/text for intent keywords (e.g., "create", "new", "init", "create
repository", "create repo", "init repo", "make repo", "repository") before
returning true, so only when isGitHubAuthenticated(runtime) is true AND the
message contains one of those intent tokens should validate return true;
implement the keyword check safely (case-insensitive, trim) and keep State and
IAgentRuntime parameters unchanged.

Comment on lines +821 to +902
try {
// Configure auth if token available
if (hasToken) {
await gitExec([
'config',
'http.https://github.com/.extraheader',
`Authorization: Basic ${Buffer.from(`x-access-token:${token}`).toString('base64')}`
], { cwd: repoDir });
}

// Fetch and checkout the branch
await gitExec(['fetch', 'origin'], { cwd: repoDir });
await gitExec(['checkout', branch], { cwd: repoDir });
await gitExec(['pull', 'origin', branch], { cwd: repoDir });
resultMessage = `Updated existing clone of ${owner}/${repo} (branch: ${branch})`;
} catch (pullError) {
logger.warn({ error: pullError }, "Pull failed, trying reset");
await gitExec(['fetch', 'origin'], { cwd: repoDir });
await gitExec(['reset', '--hard', `origin/${branch}`], { cwd: repoDir });
resultMessage = `Reset ${owner}/${repo} to origin/${branch}`;
}
} else {
// Fresh clone
operation = "clone";
const cloneArgs = ['clone'];
if (shallow) {
cloneArgs.push('--depth', '1');
}
cloneArgs.push('--branch', branch, cloneUrl, repoDir);

logger.debug(`Running: git clone ${shallow ? '--depth 1 ' : ''}--branch ${branch} [url] "${repoDir}"`);

try {
// Configure auth globally before clone if token available
if (hasToken) {
await gitExec([
'config',
'--global',
'http.https://github.com/.extraheader',
`Authorization: Basic ${Buffer.from(`x-access-token:${token}`).toString('base64')}`
]);
}

await gitExec(cloneArgs);
resultMessage = `Cloned ${owner}/${repo} to ${repoDir} (branch: ${branch}${shallow ? ", shallow" : ""})`;

// Clear global auth config after clone
if (hasToken) {
await gitExec(['config', '--global', '--unset', 'http.https://github.com/.extraheader']);
}
} catch (cloneError: any) {
// Clear auth on error too
if (hasToken) {
await gitExec(['config', '--global', '--unset', 'http.https://github.com/.extraheader']);
}

// If branch doesn't exist, try without branch specification
if (cloneError.message?.includes("Remote branch") || cloneError.message?.includes("not found")) {
logger.warn(`Branch ${branch} not found, cloning default branch`);
const fallbackArgs = ['clone'];
if (shallow) {
fallbackArgs.push('--depth', '1');
}
fallbackArgs.push(cloneUrl, repoDir);

// Configure auth again for fallback
if (hasToken) {
await gitExec([
'config',
'--global',
'http.https://github.com/.extraheader',
`Authorization: Basic ${Buffer.from(`x-access-token:${token}`).toString('base64')}`
]);
}

await gitExec(fallbackArgs);
resultMessage = `Cloned ${owner}/${repo} to ${repoDir} (default branch${shallow ? ", shallow" : ""})`;

// Clear auth after fallback
if (hasToken) {
await gitExec(['config', '--global', '--unset', 'http.https://github.com/.extraheader']);
}
Copy link

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

🏁 Script executed:

cat -n src/actions/repository.ts | sed -n '821,902p'

Repository: elizaos-plugins/plugin-github

Length of output: 4071


🏁 Script executed:

# Let's check the gitExec function to understand how it works
rg -A 10 "const gitExec|function gitExec" src/actions/repository.ts

Repository: elizaos-plugins/plugin-github

Length of output: 55


🏁 Script executed:

# Search for gitExec definition in the file
rg "gitExec" src/actions/repository.ts -B 5 -A 5 | head -50

Repository: elizaos-plugins/plugin-github

Length of output: 2050


🏁 Script executed:

# Find and examine the gitExec function definition
cat -n src/utils/shell-exec.ts | head -100

Repository: elizaos-plugins/plugin-github

Length of output: 3806


🏁 Script executed:

# Continue reading the gitExec function
cat -n src/utils/shell-exec.ts | sed -n '100,150p'

Repository: elizaos-plugins/plugin-github

Length of output: 2020


🏁 Script executed:

# Let's verify the flow and understand the context better
# Check if there are any other places where gitExec or auth is handled
rg "config.*extraheader|gitExec" src/actions/repository.ts | wc -l

Repository: elizaos-plugins/plugin-github

Length of output: 75


🏁 Script executed:

# Let me check git configuration best practices and verify the local config persistence issue
# First, let's see the full context of the pull operation
cat -n src/actions/repository.ts | sed -n '810,842p'

Repository: elizaos-plugins/plugin-github

Length of output: 1679


🏁 Script executed:

# Let's verify the actual security implications by checking git documentation
# and understanding how local vs global config works
# Also check if there's any cleanup mechanism we're missing

cat -n src/actions/repository.ts | sed -n '842,910p'

Repository: elizaos-plugins/plugin-github

Length of output: 3131


🏁 Script executed:

# Let's search for any cleanup of local config in the pull operation
rg "unset.*extraheader" src/actions/repository.ts

Repository: elizaos-plugins/plugin-github

Length of output: 380


🌐 Web query:

git -c option authentication extraheader http config security best practices

💡 Result:

What git -c … http.extraHeader=… is doing (and why it’s risky)

  • http.extraHeader tells Git to add an additional HTTP header to requests; if you configure it multiple times, all of them are sent. Setting it to an empty value resets the list. [1]
  • Any http.* option (including http.extraHeader) can be scoped to a specific URL via http.<url>.*, with matching rules for scheme/host/port (and optional wildcards for subdomains). [2]

This makes http.extraHeader convenient for passing Authorization: …, but it also creates sharp edges: secrets can be persisted in config, hard to override cleanly, and accidentally applied more broadly than intended.

Best-practice recommendations

1) Prefer Git’s credential system over extraHeader for auth

Use credential helpers / askpass flows rather than injecting auth headers:

  • Git supports credential helpers (OS keychain, cache, etc.) and other mechanisms to avoid repeatedly typing secrets. [3]

This usually avoids persisting an Authorization header in Git config at all.

2) If you must use extraHeader, scope it as narrowly as possible

  • Prefer http.https://example.com/.extraHeader=… (URL-scoped) instead of global http.extraHeader, so the header only applies to that host. [2]

3) Don’t assume -c http.…extraHeader=… “overrides” an existing header

  • Git will add multiple extraHeader entries, meaning you can end up sending two Authorization headers. This behavior has caused real-world breakage (notably with Git LFS) when attempting to “override” in CI. [4]
  • This is a known pain point with actions/checkout persisting auth via http.https://github.com/.extraheader, making later overrides difficult. [5]

4) Avoid persisting long-lived tokens; rotate and minimize privileges

  • If you’re in CI (e.g., GitHub Actions), ensure tokens are least-privilege (minimize GITHUB_TOKEN permissions / PAT scopes). [6]

5) If you do persist headers, explicitly clear them after the operation

  • Because an empty value resets extra headers, you can remove them deterministically when done (especially important in shared runners/workspaces). [1]

Sources

[1] Git config: http.extraHeader behavior (multiple headers; empty resets) (git-scm.com)
[2] Git config: URL-scoped http.<url>.* matching rules (git-scm.com)
[3] Git credentials & helpers (git-scm.com)
[4] Git LFS issue: duplicate Authorization headers when trying to override extraheader (github.com)
[5] actions/checkout issue: persisting auth in extraheader makes override difficult; credential-helper suggested (github.com)
[6] GitHub Docs: least-privilege guidance for GITHUB_TOKEN (archive.ph)

Citations:


Replace git config writes with per-command -c flag to prevent token persistence.

The code sets http.https://github.com/.extraheader in both local (pull, line 824) and global (clone, line 856) git config. This persists tokens in plaintext to .git/config or ~/.gitconfig, creating a security risk. Tokens can outlive their intended scope and be exposed if config files are logged or shared.

The pull scenario is particularly vulnerable: local config is written but never cleaned up, leaving credentials in the repository's .git/config indefinitely.

Use git's -c flag to pass auth per-command instead. This keeps secrets out of persistent config and is the recommended approach for CI/CD contexts.

🔒 Safer per-command auth (no persistent config)
-          if (hasToken) {
-            await gitExec([
-              'config',
-              'http.https://github.com/.extraheader',
-              `Authorization: Basic ${Buffer.from(`x-access-token:${token}`).toString('base64')}`
-            ], { cwd: repoDir });
-          }
+          const authArgs = hasToken
+            ? ['-c', `http.https://github.com/.extraheader=Authorization: Basic ${Buffer.from(`x-access-token:${token}`).toString('base64')}`]
+            : [];
 ...
-          await gitExec(['fetch', 'origin'], { cwd: repoDir });
+          await gitExec([...authArgs, 'fetch', 'origin'], { cwd: repoDir });
 ...
-          await gitExec(['pull', 'origin', branch], { cwd: repoDir });
+          await gitExec([...authArgs, 'pull', 'origin', branch], { cwd: repoDir });
 ...
-          if (hasToken) {
-            await gitExec([
-              'config',
-              '--global',
-              'http.https://github.com/.extraheader',
-              `Authorization: Basic ${Buffer.from(`x-access-token:${token}`).toString('base64')}`
-            ]);
-          }
-
-          await gitExec(cloneArgs);
-
-          // Clear global auth config after clone
-          if (hasToken) {
-            await gitExec(['config', '--global', '--unset', 'http.https://github.com/.extraheader']);
-          }
+          await gitExec([...authArgs, ...cloneArgs]);
🤖 Prompt for AI Agents
In `@src/actions/repository.ts` around lines 821 - 902, Summary: git config writes
persist tokens to disk; use per-command -c to avoid storing credentials. Replace
all gitExec calls that run 'config' to set http.https://github.com/.extraheader
(both local and global) with adding a per-command git config flag: prepend '-c',
`http.https://github.com/.extraheader=Authorization: Basic
${Buffer.from(\`x-access-token:${token}\`).toString('base64')}` to the gitExec
argument arrays when hasToken is true (e.g., for fetch/checkout/pull/reset in
the pull branch path and for clone/fallbackArgs in the clone branch path),
remove the corresponding gitExec(['config', ...]) and any --unset calls, and
ensure cwd and other options are preserved when you call gitExec with the
modified argument arrays (refer to gitExec, hasToken, token, cloneArgs,
fallbackArgs, repoDir, branch).

Comment on lines 32 to 36
const repoMatch = text.match(/\b([a-zA-Z0-9_-]+)\/([a-zA-Z0-9_-]+)\b/);
if (repoMatch) {
result.owner = repoMatch[1];
result.repo = repoMatch[2];
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Repository name regex doesn't match all valid GitHub identifiers.

GitHub usernames and repository names can contain dots (.), e.g., user.name/repo.js. The current pattern will fail to match these.

🔧 Proposed fix
-  const repoMatch = text.match(/\b([a-zA-Z0-9_-]+)\/([a-zA-Z0-9_-]+)\b/);
+  const repoMatch = text.match(/\b([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+)\b/);
📝 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
const repoMatch = text.match(/\b([a-zA-Z0-9_-]+)\/([a-zA-Z0-9_-]+)\b/);
if (repoMatch) {
result.owner = repoMatch[1];
result.repo = repoMatch[2];
}
const repoMatch = text.match(/\b([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+)\b/);
if (repoMatch) {
result.owner = repoMatch[1];
result.repo = repoMatch[2];
}
🤖 Prompt for AI Agents
In `@src/actions/webhooks.ts` around lines 32 - 36, The current repo extraction
using text.match(/\b([a-zA-Z0-9_-]+)\/([a-zA-Z0-9_-]+)\b/) (producing repoMatch)
fails for identifiers with dots and because \b breaks on dots; update the regex
used in the text.match call to allow dots in owner/repo (e.g. use
/([A-Za-z0-9._-]+)\/([A-Za-z0-9._-]+)/) or a non-word-boundary aware pattern (or
add appropriate lookarounds) so repoMatch[1]/repoMatch[2] correctly capture
names like user.name/repo.js.

Comment on lines 38 to 42
// Match webhook ID
const webhookMatch = text.match(/webhook\s*#?(\d+)|#(\d+)/i);
if (webhookMatch) {
result.webhookId = parseInt(webhookMatch[1] || webhookMatch[2]);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Webhook ID regex may produce false positives.

The pattern #(\d+) is overly broad and will match any #number reference (e.g., issue #123, PR #456). This could cause unintended behavior in messages that reference both webhooks and issues/PRs.

Consider making the pattern more specific or only matching #number when it appears in webhook-related context.

🔧 Proposed fix - more specific pattern
-  const webhookMatch = text.match(/webhook\s*#?(\d+)|#(\d+)/i);
+  // Only match webhook ID when explicitly mentioned with "webhook"
+  const webhookMatch = text.match(/webhook\s*(?:id\s*)?#?(\d+)/i);
   if (webhookMatch) {
-    result.webhookId = parseInt(webhookMatch[1] || webhookMatch[2]);
+    result.webhookId = parseInt(webhookMatch[1]);
   }
🤖 Prompt for AI Agents
In `@src/actions/webhooks.ts` around lines 38 - 42, The current regex in
src/actions/webhooks.ts (variable webhookMatch) is too broad because it falls
back to matching any "#123" (which can be issues/PRs); change the pattern to
only match webhooks by replacing /webhook\s*#?(\d+)|#(\d+)/i with a single more
specific pattern like /webhook\s*#?(\d+)/i, then update the parsing to use the
single capture group (use parseInt(webhookMatch[1]) and assign to
result.webhookId) so only explicit "webhook" references produce a webhookId.

Comment on lines +86 to +105
} catch (error: any) {
// execFile throws on non-zero exit codes
const exitCode = error.code || error.exitCode || 1;
const stdout = error.stdout?.toString().trim() || "";
const stderr = error.stderr?.toString().trim() || error.message || "";

logger.warn({
command,
args,
exitCode,
stderr: stderr.slice(0, 200), // Truncate for logging
}, "Shell command failed");

// For non-zero exits, return the result instead of throwing
// This allows callers to handle expected failures (e.g., git branch -D on non-existent branch)
return {
stdout,
stderr,
exitCode,
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

exitCode may be a string for system errors, violating the interface.

Node.js sets error.code to a string (e.g., 'ENOENT', 'ETIMEDOUT') for system errors, but to a number for process exit codes. The current logic could return a string exitCode, violating the number | null type contract.

🐛 Proposed fix to ensure numeric exitCode
     } catch (error: any) {
         // execFile throws on non-zero exit codes
-        const exitCode = error.code || error.exitCode || 1;
+        // error.code can be a string for system errors (e.g., 'ENOENT') or number for exit codes
+        const exitCode = typeof error.code === 'number' 
+            ? error.code 
+            : (error.exitCode ?? 1);
         const stdout = error.stdout?.toString().trim() || "";
         const stderr = error.stderr?.toString().trim() || error.message || "";
📝 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
} catch (error: any) {
// execFile throws on non-zero exit codes
const exitCode = error.code || error.exitCode || 1;
const stdout = error.stdout?.toString().trim() || "";
const stderr = error.stderr?.toString().trim() || error.message || "";
logger.warn({
command,
args,
exitCode,
stderr: stderr.slice(0, 200), // Truncate for logging
}, "Shell command failed");
// For non-zero exits, return the result instead of throwing
// This allows callers to handle expected failures (e.g., git branch -D on non-existent branch)
return {
stdout,
stderr,
exitCode,
};
} catch (error: any) {
// execFile throws on non-zero exit codes
// error.code can be a string for system errors (e.g., 'ENOENT') or number for exit codes
const exitCode = typeof error.code === 'number'
? error.code
: (error.exitCode ?? 1);
const stdout = error.stdout?.toString().trim() || "";
const stderr = error.stderr?.toString().trim() || error.message || "";
logger.warn({
command,
args,
exitCode,
stderr: stderr.slice(0, 200), // Truncate for logging
}, "Shell command failed");
// For non-zero exits, return the result instead of throwing
// This allows callers to handle expected failures (e.g., git branch -D on non-existent branch)
return {
stdout,
stderr,
exitCode,
};
🤖 Prompt for AI Agents
In `@src/utils/shell-exec.ts` around lines 86 - 105, The catch block may set
exitCode to a string (Node system error codes like 'ENOENT'), breaking the
expected number | null type; change the exitCode computation to coerce/normalize
to a numeric or null value by using a numeric-first check: if error.code is a
number use it, else if error.exitCode is a number use that, otherwise set
exitCode to null (or a numeric fallback if desired). Update the local variable
(exitCode) used in logger.warn and the returned object so it always matches
number | null; adjust references to exitCode, stdout, stderr, logger.warn, and
the returned object in the catch block accordingly.

Comment on lines +198 to +207
// Git branch names: no spaces, .., @{, \, ^, ~, :, ?, *, [
// Allow: letters, numbers, /, -, _, .
const invalidChars = /[\s@{\\^~:?*\[..]/;

if (invalidChars.test(branchName)) {
throw new Error(
`Invalid branch name: "${branchName}". ` +
`Branch names cannot contain spaces, .., @{, \\, ^, ~, :, ?, *, or [`
);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Regex incorrectly rejects single dots; .. sequence detection is broken.

Inside a character class [], .. simply adds . to the set of matched characters—it doesn't match the literal two-character sequence ... This causes valid branch names containing single dots (e.g., release/1.0.0, feature.name) to be rejected.

Git allows single dots; only the .. sequence is prohibited.

🐛 Proposed fix to correctly detect invalid patterns
     // Git branch names: no spaces, .., @{, \, ^, ~, :, ?, *, [
     // Allow: letters, numbers, /, -, _, .
-    const invalidChars = /[\s@{\\^~:?*\[..]/;
+    const invalidChars = /[\s\\^~:?*\[]/;
+    const invalidPatterns = /\.\.|@\{/;

-    if (invalidChars.test(branchName)) {
+    if (invalidChars.test(branchName) || invalidPatterns.test(branchName)) {
         throw new Error(
             `Invalid branch name: "${branchName}". ` +
             `Branch names cannot contain spaces, .., @{, \\, ^, ~, :, ?, *, or [`
         );
     }
🤖 Prompt for AI Agents
In `@src/utils/shell-exec.ts` around lines 198 - 207, The branch-name validation
currently uses the character-class invalidChars which erroneously treats '.' as
forbidden (rejecting single dots) because '..' cannot be expressed inside [];
update the validation in the branch name check (the invalidChars / branchName
test in src utils shell-exec.ts) to allow single dots but still disallow the
two-character sequence '..' and the other forbidden tokens; implement this by
making invalidChars only forbid whitespace and the single-character tokens
(e.g., use a character-class that includes @ { \\ ^ ~ : ? * [ but not .) and add
a separate test to explicitly reject the literal sequence '..' (or use a
combined regex with an alternation like \.\. to detect two dots); keep the
thrown Error message the same but ensure '.' is permitted in branch names.

Copy link

@cursor cursor bot left a comment

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 and found 3 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

// Clear global auth config after clone
if (hasToken) {
await gitExec(['config', '--global', '--unset', 'http.https://github.com/.extraheader']);
}
Copy link

Choose a reason for hiding this comment

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

Global git config modification creates race condition and token leak

High Severity

The code sets authentication headers in --global git config before cloning. This affects ALL git operations system-wide and creates two problems: (1) concurrent git processes will see/use this token, and (2) if the process crashes between setting and unsetting the config (lines 222-229), the token persists in global config indefinitely. The repo-local config approach used when the repo already exists (lines 198-202) is safer, but unavailable before .git exists. Consider using GIT_ASKPASS or credential helpers instead.

Fix in Cursor Fix in Web


// Commit the changes
await gitExec(['add', '.'], { cwd: repoDir });
await gitExec(['commit', '-m', split.description], { cwd: repoDir });
Copy link

Choose a reason for hiding this comment

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

Git command failures silently ignored due to unchecked exit codes

Medium Severity

The gitExec function returns results instead of throwing on failure (per shell-exec.ts lines 99-105), but this code never checks exitCode from the returned results. If checkout, pull, or file operations fail, execution continues silently on potentially wrong state. For example, a failed checkout safeBaseBranch means subsequent operations run on whatever branch was previously checked out, corrupting the split results.

Fix in Cursor Fix in Web

source: message.content.source,
});

const branchName = sanitizeBranchName(`split/${analysis.prNumber}/${split.name}`);
Copy link

Choose a reason for hiding this comment

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

Branch name validation outside try-catch breaks error isolation

Medium Severity

The sanitizeBranchName call at line 812 is outside the per-split try-catch block that starts at line 814. If split.name is empty (which normalizeSuggestions allows by converting missing names to ""), the branch name split/123/ ends with /, causing sanitizeBranchName to throw. This exception bypasses the per-split error handling and propagates to the outer handler, failing the entire operation even if other splits would have succeeded.

Fix in Cursor Fix in Web

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