[BUG] `argument-hint` YAML frontmatter with brackets causes React error #31 and unrecoverable TUI hang

Resolved 💬 3 comments Opened Jan 31, 2026 by b-bettis Closed Apr 17, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

Summary

When a custom skill's argument-hint frontmatter field contains YAML flow sequence syntax with a colon (e.g., [optional: path to CLAUDE.md]), the YAML parser interprets it as an array containing an object [{optional: "path to CLAUDE.md"}] instead of a literal string. When Claude Code's
Ink/React TUI attempts to render this object, it triggers Minified React error #31 ("Objects are not valid as a React child") and the TUI becomes permanently unresponsive while the process stays alive.

Environment

  • Claude Code version: 2.1.27
  • OS: Ubuntu Linux 6.8.0-90-generic
  • Runtime: Bun (bundled JavaScriptCore)

Rather than the expected behavior below, what actually happens is:

  • YAML parser interprets the bracketed value as a flow sequence containing a mapping object
  • React receives [{optional: "some hint text"}] instead of a string
  • React error #31 is thrown (objects cannot be rendered as React children)
  • TUI renderer dies with no error boundary to catch it
  • Process hangs indefinitely in ep_poll - all TCP connections remain ESTABLISHED but no useful work occurs

Root Cause

The issue stems from YAML flow sequence syntax rules. In YAML:

# What the user writes:
argument-hint: [optional: path to CLAUDE.md]

# What YAML parses it as:
argument-hint:
  - optional: "path to CLAUDE.md"

# Resulting JavaScript value:
[{optional: "path to CLAUDE.md"}]  // Array containing object

This object is then passed to an Ink/React component expecting a string primitive. React cannot render plain objects as children, triggering the unhandled error.

Why some hints work: Values without colons like [issue-number] parse as ["issue-number"] (array of strings). React can render string arrays via concatenation, so these don't crash - though they're still incorrectly typed.

Why it's unrecoverable: No React error boundary catches the rendering error. The Bun event loop continues running (epoll, timers, API connections remain active), but the TUI renderer is dead, leaving the process in a permanent hung state.

What Should Happen?

Expected Behavior

The argument-hint should be treated as a literal string "[optional: some hint text]" and rendered in the skill invocation UI without crashing.

Error Messages/Logs

ERROR  Minified React error #31; visit https://react.dev/errors/31?args[]=object%20with%20keys%20%7Boptional%7D for the full message or use the non-minified dev environment for full errors and additional helpful warnings.

 /$bunfs/root/claude:669:8731

 666 `);for(r=m=0;m<jA.length&&!jA[m].includes("DetermineComponentFrameRoot");)m++;for(;r<CL.length&&!CL[r].includes("DetermineComponentFrameRoot");)r++;if(m===jA.length||r===CL.length)for(m=jA.length-1,r=CL.length-1;1<=m&&0<=r&&jA[m]!==CL[r];)r--;for(;1<=m&&0<=r;m--,r--)if(jA[m]!==CL[r]){
 :  if(m!==1||r!==1)do if(m--,r--,0>r||jA[m]!==CL[r]){var ZI=`
 667 `+jA[m].replace(" at new "," at ");return _.displayName&&ZI.includes("<anonymous>")&&(ZI=ZI.replace("<anonymous>",_.displayName)),ZI}while(1<=m&&0<=r);break}}}finally{K8H=!1,Error.prepareStackTrace=y}return(y=_?_.displayName||_.name:"")?b(y):""}function IH(_,C){switch(_.tag){case
 :  26:case 27:case 5:return b(_.type);case 16:return b("Lazy");case 13:return _.child!==C&&C!==null?b("Suspense Fallback"):b("Suspense");case 19:return b("SuspenseList");case 0:case 15:return t(_.type,!1);case 11:return t(_.type.render,!1);case 1:return t(_.type,!0);case 31:return
    b("Activity");default:return""}}function AH(_){try{var C="",y=null;do C+=IH(_,y),y=_,_=_.return;while(_);return C}catch(m){return`
 668: Error generating stack: `+m.message+`
 669: [too long to paste - redacted]
 670:   `+(m.join(" > ")+`
 671:
 672: No matching component was found for:

 - Y1 (/$bunfs/root/claude:669:8731)
 - HI (/$bunfs/root/claude:669:10842)
 - RF (/$bunfs/root/claude:669:12411)
 - eh (/$bunfs/root/claude:669:14695)
 - <anonymous> (/$bunfs/root/claude:669:15209)
 - OG (/$bunfs/root/claude:669:35463)
 - Ss (/$bunfs/root/claude:669:51475)
 - gh (/$bunfs/root/claude:669:85493)
 - gs (/$bunfs/root/claude:669:84475)
 - cp (/$bunfs/root/claude:669:84301)

Terminated

Steps to Reproduce

Steps to Reproduce

  1. Create a custom skill file (e.g., SKILL-test.md) with this frontmatter:
---
name: test-skill
description: Test skill
argument-hint: [optional: some hint text]
---

# Test skill body
```                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   2. Invoke the skill (either via `/test-skill` or allow Claude to auto-load it)
                                                                                                                                                                                                                                                                                                   3. Observe TUI crash with error:
   ```
   ERROR Minified React error #31; visit https://react.dev/errors/31?args[]=object%20with%20keys%20%7Boptional%7D
   ```

4. TUI becomes completely unresponsive - no input accepted, process must be killed externally with `kill <pid>`


### Claude Model

Not sure / Multiple models

### Is this a regression?

I don't know

### Last Working Version

_No response_

### Claude Code Version

v2.1.27

### Platform

Other

### Operating System

Ubuntu/Debian Linux

### Terminal/Shell

iTerm2

### Additional Information

## Suggested Fixes

### 1. Type Validation on Frontmatter Parse (Primary Fix)

After YAML parsing, coerce `argument-hint` to string:

```javascript
// In frontmatter processing code
const hint = typeof frontmatter['argument-hint'] === 'string'
  ? frontmatter['argument-hint']
  : JSON.stringify(frontmatter['argument-hint']);

2. React Error Boundary

Wrap skill hint rendering in an error boundary so bad values degrade gracefully:

<ErrorBoundary fallback={<Text color="yellow">Invalid argument hint format</Text>}>
  <SkillArgumentHint value={argumentHint} />
</ErrorBoundary>

3. Documentation Update

Update examples to use quoted strings:

# Before (vulnerable):
argument-hint: [issue-number]

# After (safe):
argument-hint: "issue-number"
# or
argument-hint: "[issue-number]"

4. Frontmatter Schema Validation

Validate frontmatter types at skill load time and warn/error on type mismatches:

Warning: Skill 'test-skill' has invalid argument-hint type (object). Expected string.

Workaround

Quote the argument-hint value to force YAML to treat it as a literal string:

---
name: test-skill
description: Test skill
argument-hint: "optional: path to specific CLAUDE.md"
---

Or use single quotes:

argument-hint: '[optional: path to CLAUDE.md]'

Related Issues

This appears to be part of a broader pattern of insufficient validation in the frontmatter → React rendering pipeline:

  • Issue about frontmatter parsing with false "Missing 'name' field" errors
  • Skill discovery sensitivity to frontmatter formatting
  • Prettier-formatted frontmatter failing to parse
  • CLI crashes with Ink rendering errors

Additional Context

This bug was discovered during investigation of a hung Claude Code process that had been running for 12+ hours in an unresponsive state. The process had completed all its tasks successfully, but crashed when auto-loading a skill with argument-hint: [optional: path to specific CLAUDE.md] i
n its frontmatter.

The hang is particularly problematic because:

  1. There's no timeout or watchdog to detect the frozen TUI
  2. The process appears "alive" from the OS perspective (active network connections, event loop running)
  3. Users may not realize the session is dead and wait indefinitely for a response

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗