[BUG] `argument-hint` YAML frontmatter with brackets causes React error #31 and unrecoverable TUI hang
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
- 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:
- There's no timeout or watchdog to detect the frozen TUI
- The process appears "alive" from the OS perspective (active network connections, event loop running)
- Users may not realize the session is dead and wait indefinitely for a response
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗