Skip to content

feat(hooks): strip ANSI escape sequences from tool results in messages.transform#1990

Open
MoerAI wants to merge 6 commits intocode-yeongyu:devfrom
MoerAI:fix/strip-ansi-tool-result
Open

feat(hooks): strip ANSI escape sequences from tool results in messages.transform#1990
MoerAI wants to merge 6 commits intocode-yeongyu:devfrom
MoerAI:fix/strip-ansi-tool-result

Conversation

@MoerAI
Copy link
Contributor

@MoerAI MoerAI commented Feb 20, 2026

Summary

Closes #1796

Add a new ansi-stripper transform hook that strips ANSI escape sequences (color codes, cursor movement, etc.) from tool result outputs before they enter the LLM context window.

Problem

Tool outputs (e.g., from Bash, test runners, build tools) often contain ANSI escape sequences for terminal coloring. These raw escape codes waste context window tokens and can confuse the model's understanding of the output.

Solution

  • src/hooks/ansi-stripper/strip-ansi.ts — Pure function to strip ANSI escape sequences using a comprehensive regex covering SGR, cursor movement, 256-color, and RGB codes
  • src/hooks/ansi-stripper/hook.ts — Transform hook that iterates over all tool/tool_use parts in messages and strips ANSI from state.output and state.error fields
  • src/hooks/ansi-stripper/index.ts — Barrel export
  • src/hooks/ansi-stripper/strip-ansi.test.ts — 11 tests covering basic colors, bold/underline, 256-color, RGB, cursor movement, multi-sequence lines, and edge cases

Registration

  • Added "ansi-stripper" to HookNameSchema in src/config/schema/hooks.ts
  • Registered in src/plugin/hooks/create-transform-hooks.ts with safeCreateHook + isHookEnabled gate
  • Invoked in src/plugin/messages-transform.ts after thinkingBlockValidator
  • Exported from src/hooks/index.ts

The hook is enabled by default and can be disabled via disabled_hooks: ["ansi-stripper"] in config.

Testing

  • 11 unit tests pass (strip-ansi.test.ts)
  • TypeScript typecheck passes
  • All existing tests unaffected

Summary by cubic

Strips ANSI escape sequences from tool outputs during messages.transform to reduce token waste and prevent garbled text in the LLM context.

  • New Features

    • Added ansi-stripper transform hook for tool parts (state.output, state.error).
    • Enabled by default in messages.transform; disable via disabled_hooks: ["ansi-stripper"].
  • Bug Fixes

    • Reset regex lastIndex in stripAnsi and use non-global regex in containsAnsi to avoid stateful /g issues.
    • Use state.status discriminant with SDK ToolPart for safe field access.
    • Updated event test to spread createEventState() for new required fields.
    • Removed stale hashline-edit-diff-enhancer references to match dev.

Written for commit e1c56ca. Summary will update on new commits.

Copy link

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

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 734b41901b

ℹ️ About Codex in GitHub

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

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

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

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

}

export function containsAnsi(text: string): boolean {
return ANSI_REGEX.test(text)

Choose a reason for hiding this comment

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

P2 Badge Avoid stateful global regex in containsAnsi

containsAnsi calls .test() on ANSI_REGEX, which is declared with the global (g) flag; this makes lastIndex stateful across calls and can produce false negatives when checking multiple strings sequentially (or the same string repeatedly). Any caller that relies on containsAnsi in a loop may intermittently miss ANSI-containing text.

Useful? React with 👍 / 👎.

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

3 issues found across 9 files

Confidence score: 3/5

  • Intermittent false negatives are possible in containsAnsi because ANSI_REGEX is stateful with /g and lastIndex can skip matches across calls, which could cause user-visible detection errors.
  • Use of a custom SDKToolPart with unknown casting in src/hooks/ansi-stripper/hook.ts loosens type safety and may mask SDK incompatibilities at runtime, adding uncertainty to behavior.
  • Given the medium severity of these issues, there’s some risk of regressions, though the problems look fixable and localized.
  • Pay close attention to src/hooks/ansi-stripper/strip-ansi.ts, src/hooks/ansi-stripper/hook.ts, src/tools/hashline-edit/tools.ts - regex statefulness and SDK type workarounds.
Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/hooks/ansi-stripper/strip-ansi.ts">

<violation number="1" location="src/hooks/ansi-stripper/strip-ansi.ts:1">
P1: Stateful global regular expression causes intermittent false negatives in `containsAnsi`. The `/g` flag makes the regex stateful via `lastIndex`. When `ANSI_REGEX.test(text)` finds a match, `lastIndex` is updated, causing subsequent calls to skip past matches, resulting in false negatives.</violation>
</file>

<file name="src/tools/hashline-edit/tools.ts">

<violation number="1" location="src/tools/hashline-edit/tools.ts:182">
P2: Custom agent: **Opencode Compatibility**

The defensive runtime check for `context.metadata` is redundant. The `anomalyco/opencode` SDK strictly defines and guarantees `metadata` as a required function on `ToolContext`.</violation>
</file>

<file name="src/hooks/ansi-stripper/hook.ts">

<violation number="1" location="src/hooks/ansi-stripper/hook.ts:10">
P1: Custom agent: **Opencode Compatibility**

The PR defines a custom `SDKToolPart` interface to bypass the official SDK types, casting with `unknown` and loosely accessing `state.output` and `state.error`. According to the `@opencode-ai/sdk` types, you should use `ToolPart` (or `Extract<Part, { type: "tool" }>`) and narrow the type using the `state.status` discriminant (e.g., `status === "completed"` for `output`, or `status === "error"` for `error`).</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment on lines 182 to 184
if ("metadata" in context && typeof context.metadata === "function") {
context.metadata(meta)
}
Copy link

@cubic-dev-ai cubic-dev-ai bot Feb 20, 2026

Choose a reason for hiding this comment

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

P2: Custom agent: Opencode Compatibility

The defensive runtime check for context.metadata is redundant. The anomalyco/opencode SDK strictly defines and guarantees metadata as a required function on ToolContext.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/tools/hashline-edit/tools.ts, line 182:

<comment>The defensive runtime check for `context.metadata` is redundant. The `anomalyco/opencode` SDK strictly defines and guarantees `metadata` as a required function on `ToolContext`.</comment>

<file context>
@@ -179,7 +179,9 @@ Use \\n in text to represent literal newlines.`,
         }
 
-        context.metadata(meta)
+        if ("metadata" in context && typeof context.metadata === "function") {
+          context.metadata(meta)
+        }
</file context>
Suggested change
if ("metadata" in context && typeof context.metadata === "function") {
context.metadata(meta)
}
context.metadata(meta)
Fix with Cubic

@MoerAI MoerAI force-pushed the fix/strip-ansi-tool-result branch from 734b419 to a44db68 Compare February 20, 2026 07:46
…quences from tool results

Strip ANSI escape codes from tool_result parts in messages.transform
to prevent garbled output in the LLM context window. The hook processes
all tool parts and cleans their state.output and state.error fields.

Closes code-yeongyu#1796
…e SDK ToolPart types

Address review feedback (identified by cubic):
- Use non-global regex in containsAnsi to avoid lastIndex statefulness
- Replace custom SDKToolPart interface with SDK ToolPart type
- Use state.status discriminant for type-safe field access
… spread

EventState interface gained new required fields; the inline literal in the
session.status test was missing them, causing type errors and runtime failures.
@MoerAI MoerAI force-pushed the fix/strip-ansi-tool-result branch from d892195 to 604a1d3 Compare February 25, 2026 01:06
Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/config/schema/hooks.ts">

<violation number="1" location="src/config/schema/hooks.ts:52">
P1: Accidental deletion of 'hashline-edit-diff-enhancer' from HookNameSchema enum will break user configurations that disable this hook and create inconsistency with existing documentation and schema files.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

"write-existing-file-guard",
"anthropic-effort",
"hashline-read-enhancer",
"ansi-stripper",
Copy link

@cubic-dev-ai cubic-dev-ai bot Feb 25, 2026

Choose a reason for hiding this comment

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

P1: Accidental deletion of 'hashline-edit-diff-enhancer' from HookNameSchema enum will break user configurations that disable this hook and create inconsistency with existing documentation and schema files.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/config/schema/hooks.ts, line 52:

<comment>Accidental deletion of 'hashline-edit-diff-enhancer' from HookNameSchema enum will break user configurations that disable this hook and create inconsistency with existing documentation and schema files.</comment>

<file context>
@@ -49,7 +49,6 @@ export const HookNameSchema = z.enum([
   "anthropic-effort",
   "hashline-read-enhancer",
-  "hashline-edit-diff-enhancer",
   "ansi-stripper",
 ])
 
</file context>
Fix with Cubic

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.

[Feature]: Strip ANSI escape sequences from tool_result in messages.transform to reduce token waste

1 participant