Claude Code binary silently disables transparent huge pages (THP)

Status Fixed / completed
Maintainer reply ✓ Yes — bcherny
Activity 5 comments · opened Jun 26, 2026 · closed Aug 20, 2026
💡 Likely answer: A maintainer (bcherny, collaborator) responded on this thread — see the highlighted reply below.
Hello! Please revive this issue. Many compute/memory-intensive workloads that are run inside of Claude Code are severely degraded due to the binary's own THP flags being inherited by shell calls via fork/exec. Like the OP mentioned, this is completely silent and even explicit madvise calls from the child proc are ignored. `` systemd THP=1 └─ tmux server (new-session) THP=1 └─ -zsh (your pane shell) THP=1 └─ claude --resume THP=0 ← disabled here └─ this bash shell THP=0 ← inherits from claude ` There is an explicit call to prctl(PR_SET_THP_DISABLED, 1) somewhere on Claude Code's start-up path. My current workaround is to nudge Claude into wrapping all shell calls with a wrapper to re-enable THP: `c #include <sys/prctl.h> #include <unistd.h> int main(int argc, char **argv) { prctl(PR_SET_THP_DISABLE, 0, 0, 0, 0); execvp(argv[1], &argv[1]); return 127; } ` CLAUDE.md ` ## Transparent Huge Pages (THP) We never want a shell session running with THP *explicitly* disabled. THP should be governed only by the OS default or by a process disabling it for itself — never by an inherited disable flag. The claude binary disables THP for itself, and that flag is inherited by every command it spawns, which pessimizes compute-heavy work. Therefore wrap all shell invocations with ~/.local/bin/thp-madvise, which clears the inherited disable flag (restoring the OS default) before exec'ing the real command. For example, run ~/.local/bin/thp-madvise my-command rather than my-command. ` Gemini CLI does not have this behavior. THP for agy can be explicitly disabled with TCMALLOC_TESTONLY_DISABLE_THP_ON_INIT=1` and when it is set, a very prominent warning is shown in the CLI about the potential for degraded performance.

_Originally posted by @y4n9squared in #53471_

View original on GitHub ↗

4 Comments

yurukusa · 3 months ago

(Another user — your root-cause analysis and the prctl wrapper look right; one addition on the enforcement side.)

Wrapping each call by instructing the model in CLAUDE.md ("run ~/.local/bin/thp-madvise my-command") is the fragile part: prose instructions degrade under task momentum — the model will silently drop the wrapper on some fraction of calls, and you won't notice until a compute-heavy step is slow again. It's the same failure mode behind "Claude ignores my CLAUDE.md rule" reports.

Make it deterministic with a PreToolUse hook on Bash that rewrites the command centrally, so the model never has to remember:

# ~/.claude/hooks/thp-rewrap.sh  (PreToolUse, matcher: Bash)
cmd=$(jq -r '.tool_input.command')
case "$cmd" in
  *thp-madvise*) exit 0 ;;                 # already wrapped
esac
new=$(jq -n --arg c "$cmd" '{tool_input:{command:("~/.local/bin/thp-madvise bash -lc " + ($c|@sh))}}')
echo "$new"   # PreToolUse can return a modified tool_input

(Exact return-shape per the hooks docs — the point is the hook prepends the wrapper to every Bash invocation regardless of what the model emits, and you can drop the CLAUDE.md section entirely.) A central wrapper also avoids the per-call quoting hazards you'd hit asking the model to compose thp-madvise … my-command itself.

For the upstream report, the sharp ask is: the startup prctl(PR_SET_THP_DISABLED, 1) shouldn't be inheritable by spawned shells — either clear it before fork/exec of child commands, or gate it behind an opt-out env var. The Gemini-CLI contrast you noted is good evidence it isn't a necessary behavior. Worth confirming whether it persists with --resume vs a fresh session, since your tree shows it set on the claude --resume path specifically.

fergusfinn · 3 months ago

Hit this today, and spent some time with claude trying to figure out why. fairly confident its an upstream issue in bun & its vendored mimalloc, i'd report it there

  1. The claude CLI is a Bun-compiled binary (the "HeapHelper" threads in the prctl trace are JavaScriptCore's).
  2. With a clean inherited flag, strace shows the binary's startup doing PR_GET_THP_DISABLE → 0, then immediately PR_SET_THP_DISABLE(1). gdb places the call in an ELF initializer, not app code — and plain bun --version on your box does the same thing, while node makes no THP prctls. So it's Bun runtime behavior, not

anything claude-specific.

  1. Bun builds mimalloc with MI_DEFAULT_ALLOW_THP=0 in scripts/build/deps/mimalloc.ts (https://github.com/oven-sh/bun/blob/main/scripts/build/deps/mimalloc.ts), with the rationale in a comment: THP off saves ~20 MB peak RSS (bun --eval 1: 30 vs 52 MB) — "THP trades memory for (sometimes) latency; for a JS runtime the

memory cost isn't worth it."

  1. mimalloc implements that in src/prim/unix/prim.c (https://github.com/oven-sh/mimalloc/blob/afb4175/src/prim/unix/prim.c) as a process-wide prctl(PR_SET_THP_DISABLE, 1), with the comment "Disabling only for mimalloc process rather than touching system wide settings."

mimalloc is wrong about the scoping i think, so it applies to all downstream processes, inc. claude code, and then anything below that

y4n9squared · 3 months ago
mimalloc is wrong about the scoping i think, so it applies to all downstream processes, inc. claude code, and then anything below that

I don't think mimalloc is at fault here. My interpretation of "rather than touching system wide settings" means that it's preferring to use prctl rather than mutating the /proc/sys filesystem. The behavior that process-wide settings are inherited by child processes through fork is a general Linux behavior, not special to Bun, mimalloc, or Claude Code.

I think the issue is that Claude Code _is a shell_ -- its job is to fork. So it has unique responsibilities to ensure that child processes of the shell (i.e. user code), does not inherit idiosyncratic behavior of itself (in this case, Bun deciding that THP is bad for the app's own garbage collection).

bcherny collaborator · 1 month ago

Thanks for the detailed report and the process-tree analysis — this was accurate, and it's now fixed.

What I tested on Linux (kernel 6.18, THP policy [madvise]):

  • 2.1.173 (current when this was filed) through 2.1.228: confirmed — strace shows the binary calling prctl(PR_SET_THP_DISABLE, 1) at startup, so the process reports THP_enabled: 0 in /proc/<pid>/status and every spawned shell inherits the disable, exactly as described.
  • 2.1.233 (latest): the call is gone. A full-startup strace shows no PR_SET_THP_DISABLE, the running process reports THP_enabled: 1, and child processes inherit the OS default again — no wrapper needed.

The fix landed via a runtime update; it isn't called out in the changelog, but I verified it directly by bisecting released builds. Please upgrade to 2.1.233 or later and drop the thp-madvise workaround.

Closing as fixed — reply here if you still see THP_enabled: 0 on a current build and we'll reopen.

🤖 Generated with Claude Code

Showing cached comments. Read the full discussion on GitHub ↗