Skip to content

feat: custom tools — inject user-defined shell-command tools#648

Open
y0usaf wants to merge 9 commits intoPiebald-AI:mainfrom
y0usaf:feat/custom-tools
Open

feat: custom tools — inject user-defined shell-command tools#648
y0usaf wants to merge 9 commits intoPiebald-AI:mainfrom
y0usaf:feat/custom-tools

Conversation

@y0usaf
Copy link
Copy Markdown
Contributor

@y0usaf y0usaf commented Mar 31, 2026

Summary

  • Adds a `customTools` setting that injects user-defined, shell-backed tools into Claude Code's tool list
  • Uses CC's own `buildTool()` factory (detected via regex in the minified bundle) so custom tools are constructed exactly the same way as every native CC tool — inheriting all default method implementations automatically
  • Renders tool-use and results with `React.createElement` using detected Text/Box component variables, matching the rendering approach of every other CC tool
  • `checkPermissions` delegates to BashTool so custom tool commands match the user's existing Bash allow/deny rules (e.g. `Bash(git *)`)
  • `validateInput` type-checks declared parameters before execution
  • Two injection strategies: appends after the toolsets filter (Strategy A) or spreads into the original aggregation (Strategy B)
  • 27 tests covering helper detection, injection strategies, collision guard, rendering, and edge cases

New helpers

  • `findBuildToolFunc`: detects `function NAME(PARAM){return{...DEFAULTS,userFacingName:()=>PARAM.name,...PARAM}}` in the minified bundle
  • `getCwdFuncName`: detects CC's three-step `getCwdState → pwd → getCwd` chain for correct session working directory

Config shape

{
  "settings": {
    "customTools": [
      {
        "name": "DateTool",
        "description": "Get the current date and time",
        "parameters": {},
        "command": "date"
      },
      {
        "name": "WeatherTool",
        "description": "Get weather for a city",
        "parameters": {
          "city": { "type": "string", "description": "City name" }
        },
        "command": "curl -s wttr.in/{{city}}?format=3",
        "shell": "bash",
        "timeout": 10000
      }
    ]
  }
}

Optional fields: shell (default "sh"), timeout (default 30000 ms), workingDir, env (extra env vars), prompt (override the auto-generated LLM description).

Test plan

  • Apply with no customTools — patch skips cleanly, no change to cli.js
  • Apply with one tool, no toolsets — Strategy B fires, tool appears in Claude's tool list
  • Apply with toolsets + custom tools — Strategy A fires, custom tool appears regardless of active toolset
  • Tool with parameters — {{param}} placeholders substituted in rendered command and execution
  • Tool that exits non-zero — stderr and exit code appear in the result
  • Tool with prompt override — custom prompt string is sent to the model
  • Tool name colliding with a built-in — patch returns null with a clear error

Summary by CodeRabbit

  • New Features

    • Custom tools: add configurable user-defined tools (name, description, parameters, command, shell, timeout, working dir, env, prompt) and a patch to inject them.
  • Bug Fixes

    • Clearer native-installation error messages with native load-failure details and NixOS/Bun hints.
  • Improvements

    • Robust ELF extraction and repacking for native installations; config now validates and filters invalid custom-tool entries with warnings.
  • Tests

    • Added tests for custom-tools injection and native-installation error handling.

y0usaf added 4 commits March 6, 2026 02:48
On NixOS with Bun, node-lief fails to load because libstdc++.so.6 is
not on the system library path. Previously this produced a generic
"Could not extract JS from native binary" error with no hint about the
cause or how to fix it.

Now nativeInstallationLoader.ts tracks the load error, and
installationDetection.ts surfaces it with a NixOS-specific suggestion
to use Node.js instead (nix shell nixpkgs#nodejs -c npx tweakcc).
LIEF crashes (SIGSEGV) when parsing large ELF Bun binaries (~237MB).
On Linux/NixOS, Bun stores its data as a plain file overlay — no ELF
section parsing needed. This adds raw file-based extraction and
repacking for ELF, skipping LIEF entirely for that format.

LIEF is still used for Mach-O and PE where it reads actual binary
sections that can't be found by tail-reading alone.
- isELFFile: use try/finally to prevent fd leak; validate bytesRead
- extractBunDataFromELFRaw: correct docblock (uses BunOffsets.byteCount, not footer)
- repackELFRaw: derive elfSize from BunOffsets.byteCount instead of the
  unreliable trailing u64 footer
- nativeInstallationLoader: clear loadError on successful module load
- installationDetection: tighten NixOS hint to only trigger on shared-object
  errors (drop the over-broad 'No such file' match)
- config.test: add two tests covering enriched error message paths
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 31, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds first-class customTools (types, default setting, config validation), a patch that injects generated tool objects into Claude Code bundles (two injection strategies) with tests, and introduces ELF raw Bun extraction/repack plus native module load-error reporting.

Changes

Cohort / File(s) Summary
Types & Defaults
src/types.ts, src/defaultSettings.ts
Add CustomToolParameter and CustomTool types; extend Settings with customTools: CustomTool[]; initialize DEFAULT_SETTINGS.customTools = [].
Patch Implementation & Helpers
src/patches/customTools.ts, src/patches/helpers.ts, src/patches/index.ts
New writeCustomTools exporter generating minified tool objects (input schema, validation, renderers, call via spawnSync, permission delegation), two regex-based injection strategies (A/B), dedupe/collision checks; add findBuildToolFunc and getCwdFuncName; register custom-tools patch with conditional execution.
Config Normalization
src/config.ts
Validate/filter config.settings.customTools shape, warn and drop invalid entries, ensure array type before migrations.
Native installation & loader & detection
src/nativeInstallation.ts, src/nativeInstallationLoader.ts, src/installationDetection.ts
Add ELF raw Bun extraction (extractBunDataFromELFRaw) and repackELFRaw path with LIEF fallback; loader now records load failure and exports getNativeModuleLoadError(); extraction error messages append node-lief load reason and a NixOS/Bun hint when applicable.
Tests
src/patches/customTools.test.ts, src/tests/config.test.ts
New Vitest suite for helper detection and both injection strategies, collision/error handling, and multi-tool cases; tests for enhanced native extraction error messaging via mocked load-error strings.

Sequence Diagram

sequenceDiagram
    participant User
    participant Config
    participant Patcher
    participant Bundle
    participant Executor

    User->>Config: add settings.customTools
    Config->>Patcher: provide validated customTools
    Patcher->>Patcher: detect buildTool & getCwd names
    Patcher->>Patcher: generate tool objects (validate/dedupe)
    Patcher->>Bundle: inject generated tool objects (Strategy A/B)
    User->>Bundle: trigger tool usage
    Bundle->>Executor: call tool.call(params)
    Executor->>Executor: validate inputs, template command, set cwd/env/timeout
    Executor->>Executor: run spawnSync and collect result
    Executor-->>Bundle: return execution result
    Bundle-->>User: render tool result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • bl-ue

Poem

🐰 I nibble code and stitch a tool,
I tuck it neat into the bundle's spool,
ELF tails jump past LIEF's old song,
SpawnSync hums and hops along,
Logs wink softly: "patched and whole."

🚥 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 accurately summarizes the main feature introduced: adding user-defined shell-command tools that are injected into Claude Code's tool system.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% 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

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.

@y0usaf y0usaf force-pushed the feat/custom-tools branch from b07b3e0 to 944035a Compare March 31, 2026 20:00
Copy link
Copy Markdown
Contributor

@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: 4

🧹 Nitpick comments (1)
src/tests/config.test.ts (1)

1528-1610: Add a non-shared-object load-error case here too.

Both new tests use the same cannot open shared object message, so they only cover the branch that appends the NixOS hint. Please add one generic load failure case to prove we still include the load reason without the Nix-specific note.

As per coding guidelines, **/*.test.{ts,tsx}: Test edge cases and error conditions in test files.

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

In `@src/tests/config.test.ts` around lines 1528 - 1610, Add a third test case
similar to the two existing ones that stubs
nativeInstallation.getNativeModuleLoadError to return a non-shared-object
message (e.g. "unexpected header in node-lief") and otherwise sets up the same
mocks (whichMock, fs.stat/realpath/open, lstatSpy, mockMagicInstance.detect,
nativeInstallation.extractClaudeJsFromNativeInstallation) and then assert
findClaudeCodeInstallation(mockConfig, { interactive: true }) rejects with an
error that includes that exact load message (verifying
findClaudeCodeInstallation and nativeInstallation.getNativeModuleLoadError
handling without triggering the NixOS hint).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/config.ts`:
- Around line 223-229: The current mapping of config.settings.customTools uses
deepMergeWithDefaults(tool, DEFAULT_CUSTOM_TOOL), which backfills required
execution fields (e.g., command) and turns malformed entries into executable
placeholders; update the logic in the config.settings.customTools handling to
first validate that each tool contains required properties (such as command and
any other required metadata) and either reject/drop the entry or throw a config
error, and only apply defaults for optional fields (merge only optional keys)
for valid tools; reference the mapping that calls deepMergeWithDefaults and the
DEFAULT_CUSTOM_TOOL constant and replace it with a two-step process: validate
required fields on each tool, then selectively merge optional defaults for the
validated tool.

In `@src/nativeInstallation.ts`:
- Around line 791-799: The current branch unconditionally uses
extractBunDataFromELFRaw for any ELF (isELFFile) which breaks section-based ELF
builds; update the logic in the nativeInstallationPath handling so you first
detect the legacy overlay footer (or attempt raw parsing and check its success)
before assigning ({ bunOffsets, bunData, moduleStructSize } =
extractBunDataFromELFRaw(nativeInstallationPath)); if the raw helper fails or
the footer is not present, fall back to the LIEF path that calls
LIEF.parse(nativeInstallationPath) and getBunData(binary) (and ensure
repackELFSection remains used for section-based ELFs) so only true
legacy-overlay ELFs use the raw helpers.

In `@src/patches/customTools.ts`:
- Around line 71-72: The code currently JSON-serializes tool.shell (shellJson)
but call() always uses the POSIX '-c' flag, which breaks non-POSIX shells (e.g.,
cmd.exe). Fix by either validating tool.shell in the parsing/creation path
(reject or normalize non-POSIX shells) or, preferably, add an explicit
shellExecFlag option (e.g., shellExecFlag or execFlag) to the tool config and
use that in call() instead of hardcoding '-c'; implement a default mapping for
common shells (bash/sh -> '-c', cmd.exe -> '/c', powershell -> '-Command') and
fall back to a validation/error if unknown, and update all call() usages
(including the locations referenced around lines 145-150) to read the exec flag
instead of assuming '-c'.
- Around line 92-97: Create a single, reusable escaped substitution template and
use it everywhere instead of duplicating logic: replace the current per-site
generation (the substitutions const, the inline replacement in
renderToolUseMessage, and the uses at the later sites) with one variable like
escapedSubstitution that (1) uses a safe regex construction (escape the
parameter name before embedding it into a RegExp or build the RegExp with new
RegExp(...)) to avoid invalid regex for names like "foo/bar", and (2) uses a
consistent fallback (pick one fallback, e.g. "?" or "" and apply it everywhere).
Add or reuse a small helper escapeRegExp(name: string): string if needed, then
reference escapeRegExp(k) when building the regex and use the same fallback
expression (String(args[KEY] ?? FALLBACK)) in substitutions,
renderToolUseMessage, and the two reuse sites so the displayed and executed
commands match.

---

Nitpick comments:
In `@src/tests/config.test.ts`:
- Around line 1528-1610: Add a third test case similar to the two existing ones
that stubs nativeInstallation.getNativeModuleLoadError to return a
non-shared-object message (e.g. "unexpected header in node-lief") and otherwise
sets up the same mocks (whichMock, fs.stat/realpath/open, lstatSpy,
mockMagicInstance.detect,
nativeInstallation.extractClaudeJsFromNativeInstallation) and then assert
findClaudeCodeInstallation(mockConfig, { interactive: true }) rejects with an
error that includes that exact load message (verifying
findClaudeCodeInstallation and nativeInstallation.getNativeModuleLoadError
handling without triggering the NixOS hint).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 266a94af-94be-48b9-9740-12268989e223

📥 Commits

Reviewing files that changed from the base of the PR and between 2bd68d9 and b07b3e0.

⛔ Files ignored due to path filters (1)
  • flake.lock is excluded by !**/*.lock
📒 Files selected for processing (9)
  • src/config.ts
  • src/defaultSettings.ts
  • src/installationDetection.ts
  • src/nativeInstallation.ts
  • src/nativeInstallationLoader.ts
  • src/patches/customTools.ts
  • src/patches/index.ts
  • src/tests/config.test.ts
  • src/types.ts

Comment thread src/config.ts Outdated
Comment thread src/nativeInstallation.ts
Comment thread src/patches/customTools.ts Outdated
Comment thread src/patches/customTools.ts Outdated
Copy link
Copy Markdown
Contributor

@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.

♻️ Duplicate comments (2)
src/patches/customTools.ts (2)

145-150: ⚠️ Potential issue | 🟠 Major

The -c flag is POSIX-specific; non-POSIX shells will fail.

The shell field is configurable (e.g., users could specify cmd.exe or powershell), but the execution always passes ["-c", cmd]. This breaks on Windows shells: cmd.exe requires /c, PowerShell requires -Command.

Either validate shell to POSIX-compatible shells only, or make the exec flag configurable (e.g., add a shellExecFlag field).

🛠️ Option 1: Validate shell to POSIX-only

Add validation in src/config.ts when merging custom tools:

const ALLOWED_SHELLS = ['sh', 'bash', 'zsh', 'fish', 'dash', 'ash'];
if (tool.shell && !ALLOWED_SHELLS.includes(tool.shell)) {
  console.warn(`Custom tool "${tool.name}": shell "${tool.shell}" is not supported. Using "sh".`);
  tool.shell = 'sh';
}
🛠️ Option 2: Add shellExecFlag to CustomTool interface

Extend the interface in src/types.ts:

export interface CustomTool {
  // ... existing fields ...
  /**
   * Flag passed to shell before the command. Defaults to "-c" for POSIX shells.
   * Use "/c" for cmd.exe, "-Command" for PowerShell.
   */
  shellExecFlag?: string;
}

Then use it in the generated code:

-const result=spawnSync(${shellJson},["-c",cmd],{
+const result=spawnSync(${shellJson},[${JSON.stringify(tool.shellExecFlag ?? '-c')},cmd],{
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/patches/customTools.ts` around lines 145 - 150, Add a configurable
exec-flag instead of hardcoding ["-c", cmd]: extend the CustomTool type to
include an optional shellExecFlag (e.g., in src/types.ts/CustomTool) and update
the code that builds args for spawnSync (see spawnSync call using shellJson and
cmd in src/patches/customTools.ts) to use tool.shellExecFlag when present,
otherwise default to "-c" for POSIX shells; also add a small mapping/validation
for common shells (cmd.exe -> "/c", powershell -> "-Command") and fall back to
"-c" or warn if unknown so non-POSIX shells no longer break.

92-105: ⚠️ Potential issue | 🟠 Major

Parameter names must be escaped before embedding in regex patterns.

Parameter names are interpolated directly into regex literals without escaping. A parameter named foo/bar or param.name will produce invalid regex syntax (e.g., /\{\{foo/bar\}\}/g) and throw at runtime.

Additionally, the fallback values differ: line 95 uses "" for the executed command while line 104 uses "?" for the UI preview. This causes command drift where the user approves a different command than what executes.

🛠️ Proposed fix: escape parameter names and unify fallbacks
+// Escape special regex characters in parameter names
+const escapeRegex = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+
 // Build parameter substitution code: one replace per param
 const substitutions = Object.keys(tool.parameters)
   .map(
     k =>
-      `cmd=cmd.replace(/\\{\\{${k}\\}\\}/g,String(args[${JSON.stringify(k)}]??""));`
+      `cmd=cmd.replace(/\\{\\{${escapeRegex(k)}\\}\\}/g,String(args[${JSON.stringify(k)}]??""));`
   )
   .join('');

 // renderToolUseMessage: show the tool name and interpolated command so the
 // user can see what's about to run before approving.
 const renderToolUseMessage = `(input)=>{
   let cmd=${cmdJson};
   ${Object.keys(tool.parameters)
-    .map(k => `try{cmd=cmd.replace(/\\{\\{${k}\\}\\}/g,String(input[${JSON.stringify(k)}]??"?"));}catch(_){}`)
+    .map(k => `try{cmd=cmd.replace(/\\{\\{${escapeRegex(k)}\\}\\}/g,String(input[${JSON.stringify(k)}]??""));}catch(_){}`)
     .join('')}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/patches/customTools.ts` around lines 92 - 105, The regex interpolation
for parameter keys in the substitutions string and in renderToolUseMessage is
unsafe: escape each parameter name before embedding into the /.../g regex (e.g.,
use an escapeRegex helper to backslash-escape regex metacharacters for keys from
tool.parameters) and make the fallback value consistent between the executed
command and the preview (use the same fallback—prefer ""—for both the
substitutions (args) and renderToolUseMessage (input)); update references to
substitutions generation and the renderToolUseMessage construction (they use
cmdJson, args, input, and tool.parameters) so both use escaped keys and
identical fallbacks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/patches/customTools.ts`:
- Around line 145-150: Add a configurable exec-flag instead of hardcoding ["-c",
cmd]: extend the CustomTool type to include an optional shellExecFlag (e.g., in
src/types.ts/CustomTool) and update the code that builds args for spawnSync (see
spawnSync call using shellJson and cmd in src/patches/customTools.ts) to use
tool.shellExecFlag when present, otherwise default to "-c" for POSIX shells;
also add a small mapping/validation for common shells (cmd.exe -> "/c",
powershell -> "-Command") and fall back to "-c" or warn if unknown so non-POSIX
shells no longer break.
- Around line 92-105: The regex interpolation for parameter keys in the
substitutions string and in renderToolUseMessage is unsafe: escape each
parameter name before embedding into the /.../g regex (e.g., use an escapeRegex
helper to backslash-escape regex metacharacters for keys from tool.parameters)
and make the fallback value consistent between the executed command and the
preview (use the same fallback—prefer ""—for both the substitutions (args) and
renderToolUseMessage (input)); update references to substitutions generation and
the renderToolUseMessage construction (they use cmdJson, args, input, and
tool.parameters) so both use escaped keys and identical fallbacks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 422acb21-ad54-4746-bd4b-8a2177b98917

📥 Commits

Reviewing files that changed from the base of the PR and between b07b3e0 and 944035a.

📒 Files selected for processing (5)
  • src/config.ts
  • src/defaultSettings.ts
  • src/patches/customTools.ts
  • src/patches/index.ts
  • src/types.ts
✅ Files skipped from review due to trivial changes (2)
  • src/config.ts
  • src/patches/index.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/defaultSettings.ts
  • src/types.ts

y0usaf added 2 commits March 31, 2026 17:38
Uses CC's own buildTool() factory (detected via regex in the minified bundle)
so custom tools are constructed the same way every native CC tool is. Rendering
uses React.createElement with the detected Text/Box component variables rather
than plain strings. Permission checking delegates to BashTool so custom tool
commands match the user's existing Bash allow/deny rules.

New helpers in helpers.ts:
- getCwdFuncName: detects the minified getCwd() for correct session cwd
- findBuildToolFunc: detects the minified buildTool() factory function

Patch strategies (same as before):
- Strategy A: post-toolsets injection (appends after toolset filter)
- Strategy B: direct spread into the tool aggregation array
@y0usaf y0usaf force-pushed the feat/custom-tools branch from 944035a to 05167f3 Compare March 31, 2026 21:51
@y0usaf
Copy link
Copy Markdown
Contributor Author

y0usaf commented Mar 31, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 31, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@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: 1

♻️ Duplicate comments (2)
src/patches/customTools.ts (2)

194-199: ⚠️ Potential issue | 🟠 Major

Make the exec flag follow the configured shell.

tool.shell is configurable, but spawnSync() always receives ['-c', cmd]. That only works for POSIX shells; cmd.exe needs /c, PowerShell needs -Command, etc. Either validate tool.shell down to POSIX shells or carry an exec-flag mapping/config alongside it.

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

In `@src/patches/customTools.ts` around lines 194 - 199, The current spawnSync
invocation in customTools.ts always uses ['-c', cmd], which breaks non-POSIX
shells; update the code that builds the spawnSync args (around spawnSync, cmd,
and tool.shell) to choose the correct exec flag based on tool.shell (e.g., map
'sh'/'bash' -> '-c', 'cmd.exe' -> '/c', 'pwsh'/'powershell' -> '-Command') or
validate/restrict tool.shell to POSIX only; replace the hard-coded ['-c', cmd]
with a computed args array (using the mapping or validation) so spawnSync(spawn,
args, ...) passes the proper flag for the configured shell and preserve existing
env, cwd, timeout, and encoding behavior.

118-130: ⚠️ Potential issue | 🟠 Major

Escape placeholder keys and reuse one substitution path.

Execution/classifier/permissions substitute missing params with "", but the approval UI uses "?", so the user can approve a different command from the one that actually runs. Also, k is embedded directly into a regex literal, so keys like a.b over-match and foo/bar will generate invalid JS in the patched bundle. Build one escaped new RegExp(...) template and reuse it everywhere.

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

In `@src/patches/customTools.ts` around lines 118 - 130, The substitution helpers
(makeSubst, argsSubst, inputSubst, safeInputSubst) embed raw parameter keys into
regex literals and use different missing-value defaults ("" vs "?"), causing
over-matching/invalid JS and mismatched preview vs runtime; fix by building a
single escaped RegExp-based substitution template (escape special regex chars in
each key and construct new RegExp(escapedKey,'g')) and reuse that same
substitution code path for args and input and for safeInputSubst, and change the
safe/preview missing-param replacement to the same empty-string behavior used by
execution/classifier/permissions so the rendered approval command matches the
actual runtime command.
🧹 Nitpick comments (2)
src/patches/customTools.test.ts (1)

117-374: Add regressions for prompt overrides and non-identifier parameter names.

The suite covers the happy path well, but two brittle codegen branches still have no direct coverage: tool.prompt bypassing auto-prompt generation, and parameter keys that need regex escaping (for example a.b or foo/bar). One focused test for each would catch future breakage quickly.

As per coding guidelines "Test edge cases and error conditions in test files".

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

In `@src/patches/customTools.test.ts` around lines 117 - 374, Add two tests to the
writeCustomTools suite to cover the missing branches: (1) a test that verifies
when a CustomTool has an explicit prompt (tool.prompt) the generator uses it
rather than auto-generating one—call writeCustomTools with a tool containing a
prompt string and assert the generated result contains that prompt text
(reference writeCustomTools and tool.prompt); (2) a test that verifies parameter
keys with non-identifier characters (e.g. "a.b" and "foo/bar") are
handled/escaped correctly—construct a CustomTool whose parameters include those
keys, run writeCustomTools, and assert the output contains those exact keys or
the correctly-escaped/quoted form (reference writeCustomTools and the parameter
handling/regex-escaping logic). Ensure both tests stub console.warn/error like
the existing tests and restore spies in finally blocks.
src/patches/customTools.ts (1)

309-331: Tighten both injection regexes to the emitted minified shape.

These patterns are much looser than the rest of the patch set. For bundle patches, \s*-tolerant matching makes it easier to hit the wrong else/aggregation site or to pass tests against prettified fixtures while missing the real minified target.

Based on learnings "In patches under src/patches/, assume code works with minified, original Claude Code installations. Patch regexes must be strict and match the minified format exactly (ignore formatting differences like whitespace/newlines)."

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

In `@src/patches/customTools.ts` around lines 309 - 331, The current loose regexes
toolsetsPattern and originalPattern allow whitespace and can match
non-minified/prettified code; tighten them to the exact minified emission shape
(no \s* gaps) so we only match sequences like "}else{VAR=CALL; }let" and
"letVAR=CALL," in the minified bundle. Replace toolsetsPattern with a strict
form that disallows whitespace between tokens (e.g. match "}" "else" "{" then
capture the aggregation var and the assemble-call ending with ";" before "let")
and replace originalPattern with a strict
"let([$\w]+)=([$\w]+\([$\w]+,[$\w]+\.tools,[$\w]+\))," style regex; keep using
toolAggVar, toolsArrayCode and the same insertAt/injection logic after updating
the patterns.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/patches/customTools.ts`:
- Around line 255-262: The loop currently only checks custom tool names against
BUILTIN_TOOL_NAMES; additionally detect duplicate names among the customTools
array itself (e.g., using a Set to track seen tool.name values) and treat
duplicates as an error: when a duplicate is found log a similar error message
(referencing customTools and the conflicting tool.name) and return null to abort
injection; update the check around the for (const tool of customTools) loop that
references BUILTIN_TOOL_NAMES and tool.name to include this duplicate-name guard
so no two user-defined tools with the same name are injected.

---

Duplicate comments:
In `@src/patches/customTools.ts`:
- Around line 194-199: The current spawnSync invocation in customTools.ts always
uses ['-c', cmd], which breaks non-POSIX shells; update the code that builds the
spawnSync args (around spawnSync, cmd, and tool.shell) to choose the correct
exec flag based on tool.shell (e.g., map 'sh'/'bash' -> '-c', 'cmd.exe' -> '/c',
'pwsh'/'powershell' -> '-Command') or validate/restrict tool.shell to POSIX
only; replace the hard-coded ['-c', cmd] with a computed args array (using the
mapping or validation) so spawnSync(spawn, args, ...) passes the proper flag for
the configured shell and preserve existing env, cwd, timeout, and encoding
behavior.
- Around line 118-130: The substitution helpers (makeSubst, argsSubst,
inputSubst, safeInputSubst) embed raw parameter keys into regex literals and use
different missing-value defaults ("" vs "?"), causing over-matching/invalid JS
and mismatched preview vs runtime; fix by building a single escaped RegExp-based
substitution template (escape special regex chars in each key and construct new
RegExp(escapedKey,'g')) and reuse that same substitution code path for args and
input and for safeInputSubst, and change the safe/preview missing-param
replacement to the same empty-string behavior used by
execution/classifier/permissions so the rendered approval command matches the
actual runtime command.

---

Nitpick comments:
In `@src/patches/customTools.test.ts`:
- Around line 117-374: Add two tests to the writeCustomTools suite to cover the
missing branches: (1) a test that verifies when a CustomTool has an explicit
prompt (tool.prompt) the generator uses it rather than auto-generating one—call
writeCustomTools with a tool containing a prompt string and assert the generated
result contains that prompt text (reference writeCustomTools and tool.prompt);
(2) a test that verifies parameter keys with non-identifier characters (e.g.
"a.b" and "foo/bar") are handled/escaped correctly—construct a CustomTool whose
parameters include those keys, run writeCustomTools, and assert the output
contains those exact keys or the correctly-escaped/quoted form (reference
writeCustomTools and the parameter handling/regex-escaping logic). Ensure both
tests stub console.warn/error like the existing tests and restore spies in
finally blocks.

In `@src/patches/customTools.ts`:
- Around line 309-331: The current loose regexes toolsetsPattern and
originalPattern allow whitespace and can match non-minified/prettified code;
tighten them to the exact minified emission shape (no \s* gaps) so we only match
sequences like "}else{VAR=CALL; }let" and "letVAR=CALL," in the minified bundle.
Replace toolsetsPattern with a strict form that disallows whitespace between
tokens (e.g. match "}" "else" "{" then capture the aggregation var and the
assemble-call ending with ";" before "let") and replace originalPattern with a
strict "let([$\w]+)=([$\w]+\([$\w]+,[$\w]+\.tools,[$\w]+\))," style regex; keep
using toolAggVar, toolsArrayCode and the same insertAt/injection logic after
updating the patterns.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e6bcf5c7-6cc9-43c9-afb6-ed1ee13c8b55

📥 Commits

Reviewing files that changed from the base of the PR and between 944035a and 05167f3.

⛔ Files ignored due to path filters (1)
  • flake.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • src/defaultSettings.ts
  • src/installationDetection.ts
  • src/nativeInstallation.ts
  • src/nativeInstallationLoader.ts
  • src/patches/customTools.test.ts
  • src/patches/customTools.ts
  • src/patches/helpers.ts
  • src/patches/index.ts
  • src/tests/config.test.ts
  • src/types.ts
✅ Files skipped from review due to trivial changes (1)
  • src/patches/index.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/defaultSettings.ts
  • src/types.ts

Comment thread src/patches/customTools.ts
- customTools.ts: escape param names in regex literals to handle special chars
- customTools.ts: use consistent empty-string fallback in all substitution sites
- customTools.ts: detect duplicate custom tool names (not just built-in collisions)
- config.ts: validate required CustomTool fields when loading config; drop invalid entries
- nativeInstallation.ts: gate raw ELF path on legacy footer detection; fall back to LIEF
- config.test.ts: fix broken tests (use vi.spyOn for getNativeModuleLoadError)
- config.test.ts: add test for generic (non-shared-object) load error without NixOS hint
Copy link
Copy Markdown
Contributor

@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: 2

♻️ Duplicate comments (2)
src/patches/customTools.ts (2)

133-134: ⚠️ Potential issue | 🟠 Major

tool.shell is broader than the hardcoded -c execution path.

spawnSync(shell, ['-c', cmd]) only works for POSIX-style shells.
Values like cmd.exe, powershell, or pwsh need different flags, so
those configs are accepted but won't execute correctly. Either validate
tool.shell to POSIX shells only, or pair it with an exec-flag mapping.

♻️ Proposed fix
-  const shellJson = JSON.stringify(tool.shell ?? 'sh');
+  const shell = tool.shell ?? 'sh';
+  const execFlag =
+    /^(cmd(?:\.exe)?)$/i.test(shell)
+      ? '/c'
+      : /^(powershell(?:\.exe)?|pwsh)$/i.test(shell)
+        ? '-Command'
+        : '-c';
+  const shellJson = JSON.stringify(shell);
...
-  const result=spawnSync(${shellJson},["-c",cmd],{
+  const result=spawnSync(${shellJson},[${JSON.stringify(execFlag)},cmd],{

Also applies to: 244-254

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

In `@src/patches/customTools.ts` around lines 133 - 134, The code currently
stringifies tool.shell into shellJson and then runs spawnSync(shell, ['-c',
cmd]) which only works for POSIX shells; update the execution to derive the
correct execution flag from the shell value (e.g., '-c' for sh/bash, '/c' for
cmd.exe, '-Command' or '-c' for PowerShell variants) by adding a small helper
like getShellExecFlag(shell) and use spawnSync(shell, [execFlag, cmd]); validate
the returned execFlag (throw or log and skip if unknown) so configs like
cmd.exe, powershell, and pwsh execute correctly; apply the same change where
shellJson/timeoutVal and spawnSync are used elsewhere (the other block
referenced around the later range).

156-179: ⚠️ Potential issue | 🟠 Major

Stop emitting regex literals for placeholder replacement.

escapeForRegex() still leaves / untouched, so a parameter like
foo/bar produces invalid generated code
(/\{\{foo/bar\}\}/g) and can break the patched bundle. Build the
matcher with new RegExp(...) instead of a regex literal, or escape the
literal delimiter too.

♻️ Proposed fix
   const escapeForRegex = (s: string): string =>
     s.replace(/[.*+?^${}()|[\]\\]/g, '\\\\$&');
+  const makeRegex = (name: string): string =>
+    `new RegExp(${JSON.stringify(`\\{\\{${escapeForRegex(name)}\\}\\}`)},'g')`;

   const makeSubst = (varName: string): string =>
     Object.keys(tool.parameters)
       .map(
         k =>
-          `cmd=cmd.replace(/\\{\\{${escapeForRegex(k)}\\}\\}/g,String(${varName}[${JSON.stringify(k)}]??""));`
+          `cmd=cmd.replace(${makeRegex(k)},String(${varName}[${JSON.stringify(k)}]??""));`
       )
       .join('');

   const safeInputSubst = Object.keys(tool.parameters)
     .map(
       k =>
-        `try{cmd=cmd.replace(/\\{\\{${escapeForRegex(k)}\\}\\}/g,String(input[${JSON.stringify(k)}]??""));}catch(_){}`
+        `try{cmd=cmd.replace(${makeRegex(k)},String(input[${JSON.stringify(k)}]??""));}catch(_){}`
     )
     .join('');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/patches/customTools.ts` around lines 156 - 179, The generated replacement
currently emits regex literals (see makeSubst, argsSubst, inputSubst,
safeInputSubst and escapeForRegex) which fails for parameter names containing
'/'; stop emitting regex literals and build matchers with the RegExp constructor
instead. Update the template generation in makeSubst and the safeInputSubst
mapping to produce code that calls new RegExp(...) with a properly escaped
pattern string (escapeForRegex should either also escape '/' or you should
escape it when interpolating into the string passed to RegExp) and the 'g' flag,
and keep the same String(...??"") fallback logic; ensure the same change is
applied where safeInputSubst currently emits try/catch per param.
🧹 Nitpick comments (1)
src/nativeInstallation.ts (1)

1520-1541: Consider extracting atomic file write logic to reduce duplication.

The temp-file-then-rename pattern with busy-file error handling is duplicated from atomicWriteBinary. A shared helper could simplify maintenance.

♻️ Example extraction
function atomicWriteBuffer(
  buffer: Buffer,
  outputPath: string,
  originalPath: string
): void {
  const tempPath = outputPath + '.tmp';
  fs.writeFileSync(tempPath, buffer);

  const origStat = fs.statSync(originalPath);
  fs.chmodSync(tempPath, origStat.mode);

  try {
    fs.renameSync(tempPath, outputPath);
  } catch (error) {
    try {
      fs.unlinkSync(tempPath);
    } catch {
      // ignore cleanup errors
    }
    if (
      error instanceof Error &&
      'code' in error &&
      (error.code === 'ETXTBSY' ||
        error.code === 'EBUSY' ||
        error.code === 'EPERM')
    ) {
      throw new Error(
        'Cannot update the Claude executable while it is running.\n' +
          'Please close all Claude instances and try again.'
      );
    }
    throw error;
  }
}

Then repackELFRaw becomes:

-  const tempPath = outputPath + '.tmp';
-  fs.writeFileSync(tempPath, newBinary);
-  const origStat = fs.statSync(binPath);
-  fs.chmodSync(tempPath, origStat.mode);
-  try {
-    fs.renameSync(tempPath, outputPath);
-  } catch (error) {
-    try {
-      fs.unlinkSync(tempPath);
-    } catch {
-      // ignore cleanup errors
-    }
-    if (
-      error instanceof Error &&
-      'code' in error &&
-      (error.code === 'ETXTBSY' ||
-        error.code === 'EBUSY' ||
-        error.code === 'EPERM')
-    ) {
-      throw new Error(
-        'Cannot update the Claude executable while it is running.\n' +
-          'Please close all Claude instances and try again.'
-      );
-    }
-    throw error;
-  }
+  atomicWriteBuffer(newBinary, outputPath, binPath);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/nativeInstallation.ts` around lines 1520 - 1541, The
temp-file-then-rename pattern and busy-file error handling in repackELFRaw is
duplicated from atomicWriteBinary; extract this into a shared helper (e.g.,
atomicWriteBuffer or atomicReplaceFile) that takes the Buffer (or source path),
outputPath and originalPath, performs write to tempPath, copies mode from
original via fs.statSync+fs.chmodSync, attempts fs.renameSync and on failure
unlinks tempPath and maps ETXTBSY/EBUSY/EPERM to the existing "Cannot update the
Claude executable while it is running" Error; then replace the duplicated logic
in repackELFRaw (and any other caller) to call the new helper and remove the
duplicated try/catch block around renameSync.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/config.ts`:
- Around line 230-245: The validation assumes config.settings.customTools is a
well-formed array of objects and uses JSON.stringify(tool) which can leak
secrets; instead first guard that config.settings.customTools is an Array (if
not, set to []), then iterate with index (e.g., replace the .filter call) and
for each entry validate types explicitly: ensure typeof tool === "object" &&
tool !== null, typeof tool.name === "string" && tool.name.trim() !== "", typeof
tool.command === "string", typeof tool.description === "string", and that
tool.parameters is the expected shape (use Array.isArray(tool.parameters) or
typeof tool.parameters === "object" depending on what writeCustomTools()
expects). For entries that fail, log only the index, the tool.name (if present),
and a list of the invalid/missing keys (do not JSON.stringify the whole tool),
and skip/drop those entries so writeCustomTools() receives only well-typed
tools.

In `@src/tests/config.test.ts`:
- Around line 1562-1568: The test currently only checks the generic error
prefix; update the assertion so it verifies the concrete load reason returned by
the mock (e.g. assert the error message contains the unique substring
"libstdc++.so.6") by changing the expectation on findClaudeCodeInstallation
(which is mocked via vi.spyOn(nativeInstallation, 'getNativeModuleLoadError')
and uses mockConfig) to check for that specific substring (use rejects.toThrow
with the full/partial string or rejects.toMatch if preferred) so the test fails
if the detailed reason is dropped.

---

Duplicate comments:
In `@src/patches/customTools.ts`:
- Around line 133-134: The code currently stringifies tool.shell into shellJson
and then runs spawnSync(shell, ['-c', cmd]) which only works for POSIX shells;
update the execution to derive the correct execution flag from the shell value
(e.g., '-c' for sh/bash, '/c' for cmd.exe, '-Command' or '-c' for PowerShell
variants) by adding a small helper like getShellExecFlag(shell) and use
spawnSync(shell, [execFlag, cmd]); validate the returned execFlag (throw or log
and skip if unknown) so configs like cmd.exe, powershell, and pwsh execute
correctly; apply the same change where shellJson/timeoutVal and spawnSync are
used elsewhere (the other block referenced around the later range).
- Around line 156-179: The generated replacement currently emits regex literals
(see makeSubst, argsSubst, inputSubst, safeInputSubst and escapeForRegex) which
fails for parameter names containing '/'; stop emitting regex literals and build
matchers with the RegExp constructor instead. Update the template generation in
makeSubst and the safeInputSubst mapping to produce code that calls new
RegExp(...) with a properly escaped pattern string (escapeForRegex should either
also escape '/' or you should escape it when interpolating into the string
passed to RegExp) and the 'g' flag, and keep the same String(...??"") fallback
logic; ensure the same change is applied where safeInputSubst currently emits
try/catch per param.

---

Nitpick comments:
In `@src/nativeInstallation.ts`:
- Around line 1520-1541: The temp-file-then-rename pattern and busy-file error
handling in repackELFRaw is duplicated from atomicWriteBinary; extract this into
a shared helper (e.g., atomicWriteBuffer or atomicReplaceFile) that takes the
Buffer (or source path), outputPath and originalPath, performs write to
tempPath, copies mode from original via fs.statSync+fs.chmodSync, attempts
fs.renameSync and on failure unlinks tempPath and maps ETXTBSY/EBUSY/EPERM to
the existing "Cannot update the Claude executable while it is running" Error;
then replace the duplicated logic in repackELFRaw (and any other caller) to call
the new helper and remove the duplicated try/catch block around renameSync.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ecfb591c-710c-4ab1-9359-8f6dbf77dc0d

📥 Commits

Reviewing files that changed from the base of the PR and between 05167f3 and 4291e1c.

📒 Files selected for processing (4)
  • src/config.ts
  • src/nativeInstallation.ts
  • src/patches/customTools.ts
  • src/tests/config.test.ts

Comment thread src/config.ts Outdated
Comment thread src/tests/config.test.ts Outdated
@y0usaf
Copy link
Copy Markdown
Contributor Author

y0usaf commented Mar 31, 2026

Addressed all review comments:

  • Regex escaping: Added escapeForRegex helper; all three substitution sites now escape param names and use consistent empty-string fallback
  • Duplicate tool names: Added seenToolNames Set check in the validation loop
  • ELF raw path: Both extract and repack functions now try the raw path first, catch any error, and fall back to LIEF for section-based ELF builds
  • Shell exec flag: Generated spawnSync call now maps shell basename to correct flag (cmd/c, powershell/pwsh-Command, others → -c)
  • Config validation: customTools validation now guards array container, checks types explicitly, logs index/name/invalid-keys only (no secret leakage), rejects non-object parameters
  • Test assertion: Updated to assert concrete error substring 'Reason: node-lief failed to load: libstdc++.so.6'
  • New test: Added generic (non-shared-object) load error test; also fixed the two pre-existing test failures using vi.spyOn instead of vi.mocked

@y0usaf
Copy link
Copy Markdown
Contributor Author

y0usaf commented Apr 9, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 9, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@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: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/patches/customTools.ts`:
- Around line 67-70: The generatePromptString function currently treats a falsy
prompt as missing, so a deliberate empty string prompt gets ignored; change the
check to explicitly test for undefined (e.g., if (tool.prompt !== undefined)) so
that an explicit '' is honored and returned; update the logic in
generatePromptString that references CustomTool.prompt to return tool.prompt
when it is defined rather than when it is truthy.
- Around line 218-236: The generated handlers (inputSchema.safeParse/parse,
toAutoClassifierInput, checkPermissions, and validateInput) assume input/args
are objects/arrays and can throw on null/undefined; ensure you normalize at the
top of those functions by coercing input to a safe default (e.g., input = input
?? {} and const args = Array.isArray(input.args) ? input.args : []) before any
indexing or substitutions, and update toAutoClassifierInput and checkPermissions
to use the normalized variables (refer to the functions/methods named
inputSchema:{safeParse,parse}, toAutoClassifierInput, checkPermissions, and
validateInput) so validateInput can return structured errors instead of runtime
TypeErrors.
- Around line 175-193: makeSubst and safeInputSubst currently pass a string to
String.prototype.replace which allows $-sequences in parameter values to be
interpreted; change the generated replacement to use a function callback so
values are inserted literally. Update makeSubst's generation from
`cmd=cmd.replace(/\\{\\{${escapeForRegex(k)}\\}\\}/g,String(${varName}[${JSON.stringify(k)}]??""));`
to use a replacer function like
`cmd=cmd.replace(/\\{\\{${escapeForRegex(k)}\\}\\}/g,()=>String(${varName}[${JSON.stringify(k)}]??""));`
and change each entry in safeInputSubst similarly from
`cmd=cmd.replace(...,String(input[...]))` to
`cmd=cmd.replace(...,()=>String(input[...]??""));` so argsSubst, inputSubst and
safeInputSubst all use function callbacks and avoid `$`-sequence interpretation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f51ed78f-4543-4bf3-94d2-79ccfe7c83a3

📥 Commits

Reviewing files that changed from the base of the PR and between 4291e1c and 9eeaf82.

📒 Files selected for processing (3)
  • src/config.ts
  • src/patches/customTools.ts
  • src/tests/config.test.ts
✅ Files skipped from review due to trivial changes (2)
  • src/config.ts
  • src/tests/config.test.ts

Comment on lines +67 to +70
const generatePromptString = (tool: CustomTool): string => {
if (tool.prompt) {
return tool.prompt;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Honor an explicit empty prompt override.

if (tool.prompt) treats prompt: '' as “no override” and regenerates the default prompt instead. Since CustomTool.prompt is typed as string | undefined, this should check for undefined.

Proposed fix
-  if (tool.prompt) {
+  if (tool.prompt !== undefined) {
     return tool.prompt;
   }
📝 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 generatePromptString = (tool: CustomTool): string => {
if (tool.prompt) {
return tool.prompt;
}
const generatePromptString = (tool: CustomTool): string => {
if (tool.prompt !== undefined) {
return tool.prompt;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/patches/customTools.ts` around lines 67 - 70, The generatePromptString
function currently treats a falsy prompt as missing, so a deliberate empty
string prompt gets ignored; change the check to explicitly test for undefined
(e.g., if (tool.prompt !== undefined)) so that an explicit '' is honored and
returned; update the logic in generatePromptString that references
CustomTool.prompt to return tool.prompt when it is defined rather than when it
is truthy.

Comment on lines +175 to +193
const makeSubst = (varName: string): string =>
Object.keys(tool.parameters)
.map(
k =>
`cmd=cmd.replace(/\\{\\{${escapeForRegex(k)}\\}\\}/g,String(${varName}[${JSON.stringify(k)}]??""));`
)
.join('');
const argsSubst = makeSubst('args');
const inputSubst = makeSubst('input');

// Safe substitution (try/catch per param) for renderToolUseMessage, which
// receives partial input while the model is still streaming parameters.
// Uses the same empty-string fallback as argsSubst/inputSubst for consistency.
const safeInputSubst = Object.keys(tool.parameters)
.map(
k =>
`try{cmd=cmd.replace(/\\{\\{${escapeForRegex(k)}\\}\\}/g,String(input[${JSON.stringify(k)}]??""));}catch(_){}`
)
.join('');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify that JS replacement strings rewrite $-sequences instead of inserting them literally.
node - <<'NODE'
const template = 'echo {{arg}}';
for (const value of ['$$', '$&', '$`', "$'"]) {
  const out = template.replace(/\{\{arg\}\}/g, String(value));
  console.log(JSON.stringify(value), '=>', JSON.stringify(out));
}
NODE

Repository: Piebald-AI/tweakcc

Length of output: 138


🏁 Script executed:

sed -n '175,193p' src/patches/customTools.ts

Repository: Piebald-AI/tweakcc

Length of output: 834


🏁 Script executed:

rg 'replace\(' src/patches/customTools.ts -A 2 -B 2

Repository: Piebald-AI/tweakcc

Length of output: 753


🏁 Script executed:

node - <<'NODE'
// Test both string replacement and callback replacement
const template = 'echo {{arg}}';
const testValues = ['$$', '$&', '$`', "$'", 'normal', 'test$value'];

console.log('=== String Replacement (VULNERABLE) ===');
for (const value of testValues) {
  const out = template.replace(/\{\{arg\}\}/g, String(value));
  console.log(JSON.stringify(value).padEnd(15), '=>', JSON.stringify(out));
}

console.log('\n=== Callback Replacement (PROPOSED FIX) ===');
for (const value of testValues) {
  const out = template.replace(/\{\{arg\}\}/g, () => String(value));
  console.log(JSON.stringify(value).padEnd(15), '=>', JSON.stringify(out));
}
NODE

Repository: Piebald-AI/tweakcc

Length of output: 523


🏁 Script executed:

# Check if custom tool parameters in this codebase might contain $ sequences
# Look for tool parameter examples or definitions
rg 'parameters.*:' src/patches/customTools.ts -A 5 -B 2 | head -40

Repository: Piebald-AI/tweakcc

Length of output: 44


Use a replacer callback to prevent $-sequence interpretation in parameter values.

When String.prototype.replace() receives a string as the replacement argument, it interprets special sequences like $$, $&, $``, and $'rather than inserting the value literally. A parameter containing$$(e.g., from a shell variable reference) would be corrupted:$$→$, $&` → matched text, etc. Use an arrow function callback instead to preserve values exactly.

This affects both makeSubst() and safeInputSubst().

Proposed fix
 const makeSubst = (varName: string): string =>
   Object.keys(tool.parameters)
     .map(
       k =>
-          `cmd=cmd.replace(/\\{\\{${escapeForRegex(k)}\\}\\}/g,String(${varName}[${JSON.stringify(k)}]??""));`
+          `cmd=cmd.replace(/\\{\\{${escapeForRegex(k)}\\}\\}/g,()=>String(${varName}[${JSON.stringify(k)}]??""));`
     )
     .join('');
@@
 const safeInputSubst = Object.keys(tool.parameters)
   .map(
     k =>
-        `try{cmd=cmd.replace(/\\{\\{${escapeForRegex(k)}\\}\\}/g,String(input[${JSON.stringify(k)}]??""));}catch(_){}`
+        `try{cmd=cmd.replace(/\\{\\{${escapeForRegex(k)}\\}\\}/g,()=>String(input[${JSON.stringify(k)}]??""));}catch(_){}`
   )
   .join('');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/patches/customTools.ts` around lines 175 - 193, makeSubst and
safeInputSubst currently pass a string to String.prototype.replace which allows
$-sequences in parameter values to be interpreted; change the generated
replacement to use a function callback so values are inserted literally. Update
makeSubst's generation from
`cmd=cmd.replace(/\\{\\{${escapeForRegex(k)}\\}\\}/g,String(${varName}[${JSON.stringify(k)}]??""));`
to use a replacer function like
`cmd=cmd.replace(/\\{\\{${escapeForRegex(k)}\\}\\}/g,()=>String(${varName}[${JSON.stringify(k)}]??""));`
and change each entry in safeInputSubst similarly from
`cmd=cmd.replace(...,String(input[...]))` to
`cmd=cmd.replace(...,()=>String(input[...]??""));` so argsSubst, inputSubst and
safeInputSubst all use function callbacks and avoid `$`-sequence interpretation.

Comment on lines +218 to +236
inputSchema:{safeParse:(i)=>({success:true,data:i}),parse:(i)=>i},
async description(){return ${descJson}},
async prompt(){return ${promptJson}},
isConcurrencySafe(){return false},
isReadOnly(){return false},
toAutoClassifierInput(input){
let cmd=${cmdJson};
${inputSubst}
return cmd;
},
checkPermissions(input,context){
let cmd=${cmdJson};
${inputSubst}
const bashTool=context.options.tools.find(t=>t.name==="Bash");
if(bashTool)return bashTool.checkPermissions({command:cmd,timeout:${timeoutVal}},context);
return Promise.resolve({behavior:"passthrough",message:"Permission required to run "+${nameJson}});
},
async validateInput(input){
${paramValidations}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the generated code dereferences input/args directly, and that nullish indexing throws.
sed -n '218,236p' src/patches/customTools.ts
sed -n '256,268p' src/patches/customTools.ts
node - <<'NODE'
for (const value of [undefined, null]) {
  try {
    console.log(value['toolArg']);
  } catch (err) {
    console.log(err.name + ': ' + err.message);
  }
}
NODE

Repository: Piebald-AI/tweakcc

Length of output: 1403


Normalize input/args before indexing into them.

inputSchema.parse/safeParse accept anything, but the generated code immediately reads input[...] / args[...]. null or undefined payloads will throw TypeError before validateInput() can return a structured error.

Proposed fix
 inputSchema:{safeParse:(i)=>({success:true,data:i}),parse:(i)=>i},
 async description(){return ${descJson}},
 async prompt(){return ${promptJson}},
 isConcurrencySafe(){return false},
 isReadOnly(){return false},
 toAutoClassifierInput(input){
+  input=typeof input==="object"&&input!==null?input:{};
   let cmd=${cmdJson};
   ${inputSubst}
   return cmd;
 },
 checkPermissions(input,context){
+  input=typeof input==="object"&&input!==null?input:{};
   let cmd=${cmdJson};
   ${inputSubst}
   const bashTool=context.options.tools.find(t=>t.name==="Bash");
   if(bashTool)return bashTool.checkPermissions({command:cmd,timeout:${timeoutVal}},context);
   return Promise.resolve({behavior:"passthrough",message:"Permission required to run "+${nameJson}});
 },
 async validateInput(input){
+  if(typeof input!=="object"||input===null||Array.isArray(input))
+    return{result:false,message:"input must be an object",errorCode:1};
   ${paramValidations}
   return{result:true};
 },
@@
 async call(args){
+  args=typeof args==="object"&&args!==null?args:{};
   let cmd=${cmdJson};
   ${argsSubst}
   const {spawnSync}=${requireFunc}("child_process");

Applies to lines 218-236 and 256-268.

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

In `@src/patches/customTools.ts` around lines 218 - 236, The generated handlers
(inputSchema.safeParse/parse, toAutoClassifierInput, checkPermissions, and
validateInput) assume input/args are objects/arrays and can throw on
null/undefined; ensure you normalize at the top of those functions by coercing
input to a safe default (e.g., input = input ?? {} and const args =
Array.isArray(input.args) ? input.args : []) before any indexing or
substitutions, and update toAutoClassifierInput and checkPermissions to use the
normalized variables (refer to the functions/methods named
inputSchema:{safeParse,parse}, toAutoClassifierInput, checkPermissions, and
validateInput) so validateInput can return structured errors instead of runtime
TypeErrors.

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