Running claude in one specific project directory immediately returns the shell prompt with:
[1] + 53014 suspended (tty input) claude
- Happens only in that directory; works fine elsewhere.
claude --versionandclaude --helpwork normally — the issue is only during interactive startup.- Typing
fgresumes claude and it runs normally:✦ ➜ fg [1] + 5551 continued claude
A process receives SIGTTIN when it tries to read from the controlling terminal while not in the terminal's foreground process group. The shell catches that and prints suspended (tty input). So claude is starting up, somehow losing the TTY foreground group, then attempting to read input → suspended.
This is not a tty-mode issue (stty tostop is not involved — that's tty output).
Stale per-project state in ~/.claude.json. Claude Code keeps a projects map in that file, keyed by absolute path. The entry for the directory contained corrupt/stale values from a prior session (in my case, lastFpsAverage: 0.65, an uninstall first-prompt, and other leftovers). Reading that entry on launch put claude into a state where it lost TTY foreground during UI bring-up.
Files that are not the cause and can be skipped during diagnosis:
.mcp.jsonCLAUDE.md.claude/settings.local.json.claude/skills/- Shell rc files
- Terminal emulator
- Project source / git state
I confirmed this by copying .claude/ and CLAUDE.md into /tmp/test/ — claude started fine. The trigger lives in ~/.claude.json, not the project.
Quit claude in all terminals first (it rewrites ~/.claude.json on exit and would clobber the edit).
Back up, then delete the offending project entry:
cp ~/.claude.json ~/.claude.json.bak
python3 -c "
import json
with open('/Users/USERNAME/.claude.json') as f: d = json.load(f)
removed = d['projects'].pop('/absolute/path/to/project', None)
with open('/Users/USERNAME/.claude.json','w') as f: json.dump(d, f, indent=2)
print('Removed:', removed is not None)
"Replace /Users/USERNAME/.claude.json and /absolute/path/to/project with real paths.
Launch claude in the directory again. Trust dialog and onboarding re-run once, then it's fine.
Before touching anything, confirm the pattern:
- Directory-specific?
cd ~ && claude— works? → yes, directory-specific. - Interactive-only?
claude --versionreturns normally? → yes, interactive startup is the trigger. fgrecovers it? After the suspend, typefg— claude resumes cleanly? → yes, it's a TTY foreground loss, not a crash.- Project config innocent? Copy
.claude/andCLAUDE.mdto/tmp/fooand run claude there — works fine? → project files are not the cause.
If all four match, the stale ~/.claude.json entry is almost certainly it.
If your symptom diverges, here's the broader diagnostic tree:
.mcp.jsonMCP servers — rename to.mcp.json.off, retry. An MCP server stealing the TTY foreground group viatcsetpgrp()would do this.- Shell wrapper / alias —
type claudeshould show/path/to/claude, not a function or alias. stty tostop—stty -a | grep tostop. Iftostopis set,stty -tostop.- Lost job control —
setopt | grep monitor(zsh) should showmonitor. If not,setopt MONITOR. - Inherited bad process group — open a fresh terminal window before testing.
- Capture startup output —
script /tmp/claude.log claude, then inspect/tmp/claude.logfor anything claude printed before suspending.
No real prevention — this is a Claude Code bug (corrupt project state causing interactive-startup TTY mishandling). The state file isn't user-facing, so it can drift between versions or after crashes.
If you hit it again in another directory:
- Same recipe: pop that project's entry from
~/.claude.json. - Keep
~/.claude.json.bakaround in case you ever need to recover the old state.
# claude is suspended on launch in one specific dir → ~/.claude.json has bad state for it
cp ~/.claude.json ~/.claude.json.bak
python3 -c "
import json
p = '/Users/USERNAME/.claude.json'
target = '/absolute/path/to/project'
with open(p) as f: d = json.load(f)
d['projects'].pop(target, None)
with open(p,'w') as f: json.dump(d, f, indent=2)
"
# launch claude again in the dir — trust prompt + onboarding once, then fine