Skip to content

Mods - make Claude 10x more extensible #91870

Open

Description

@poteat

Community Micro-Update: Oct 1, 2026

We're live! We're rapidly working down the latest elements of feedback folks have posted. Thank you all again for being amazing stewards of this new extensibility affordance. 🚀

Community Update: Sep 9, 2026

AI;DR: We're shipping in N weeks.

Thank you all for the positive feedback, and especially for the insightful, high-signal feedback that has materially shaped our design.

We're now committed to shipping function hooks, on the scale of weeks in lieu of days or months. As well, from a product perspective, we are going to be calling this functionality "Claude Mods". The engineering term of art 'function hook' will still exist as the documented implementation primitive Mods are built on. A mod is just a plugin that uses function hooks, nothing is changing there. Hopefully the ontology is not too confusing.

We're still rapidly iterating on the interface, design, etc. but much of the semantics are now set in place, and we don't anticipate as many breaking changes as our first week. Therefore, we've made available our source listings for our first three built-in mods, perusable here. Our intent is to take further extant features as they exist in CC today and migrate them to mod form.

Finally, I'm sharing here a cheat sheet reference that enumerates some affordances on v267/v268 (today / tomorrow). As well, I may now publicly acknowledge that any folks who want to test and give feedback may use CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 claude.

We are still iterating and it is only more important for the community to engage and give critical feedback to ensure this ends up in the right shape. Thank you all.

Image

Original Post: Sep 3, 2026

Hi folks! ✨

AI;DR: Function Hooks let you modify CC very deeply, while still being safe through side-effect tracking over a parameterized $ object, and while composing neatly using a registration-order-based 'next' continuation model a la Express, or Koa.

I was asked to get feedback from the community regarding an internal proposal we have.

Basically, it would be neat if you could hook into CC using TypeScript functions a la Express (or Koa!), and basically change whatever you want. So that's what we're proposing. In the workplace, admins will be able to change what they want too, including your ability to change things, to a very fine level of programmatic detail.

You should even be able to modify how components get rendered. We use React across the board, so why not utilize that fact?

There's a technical architecture doc, that goes through some of the design. As well, I made a few videos for internal folks so they could see how it might feel, which I'm sharing here too. I've tried to ensure that they're informative and engaging.

Algebraically, this is just an effect-parameterized endomorphic continuation model for plugins.

If you want this, please engage here - the response from the community likely dictates whether this ships or not. The $ sigil isn't negotiable though, I follow a more ancient god.

Basic Series

You can either click on the thumbnails or the inline video toggles below. Each Basic and Advanced video is 60 seconds; the full Case Study videos are around 2 minutes long and show more comprehensive uses of function hooks to form cohesive features.

1-a-hook-as-a-function.mp4
2-a-hook-that-says-no.mp4
3-plugins-can-draw-now.mp4
4-admin-control-as-a-hook.mp4

⚠️ GitHub mutes videos by default, so be sure to turn sound on.

The four videos
 ⓵ A hook as a function

We introduce the ability for hooks to be TypeScript functions, with full type definitions and LSP support.

1-a-hook-as-a-function.mp4
 ⓶ A hook that says no

Function hooks may be used to restrict behavior, for safety and control, as you're used to.

2-a-hook-that-says-no.mp4
 ⓷ Plugins can draw now

You can hook onto components and modify their props or wrap their returned render nodes.

3-plugins-can-draw-now.mp4
 ⓸ Admin control as a hook

Admins can remove affordances from $ so that all plugins below cannot invoke that side-effect.

4-admin-control-as-a-hook.mp4

Advanced Series

5-order-is-nesting.mp4
6-press-a-plugins-button.mp4
7-every-event-at-once.mp4
The three videos
 ⓹ Order is nesting

Plugins nest like middleware. The first one registered wraps the rest, so admins prepend for control and append for defaults.

5-order-is-nesting.mp4
 ⓺ Press a plugin's button

Hooks catch interactions too. One hook on ui.press sees the same button pressed in the terminal and in the desktop app.

6-press-a-plugins-button.mp4
 ⓻ Every event at once

A single hook on * sees every event, including every plugin's own calls on $, so an audit log is one function.

7-every-event-at-once.mp4

Case Studies

8-one-sentence-one-plugin.mp4
9-change-what-claude-code-shows.mp4
The two videos
 ⓼ One sentence. One plugin

From one short ask, Claude writes, validates and loads a plugin that replaces secrets in tool output before the model reads them.

8-one-sentence-one-plugin.mp4
 ⓽ Change what Claude Code shows

A plugin hides sensitive values in Claude Code Desktop until you hover over them, so you can share your screen without showing your data.

9-change-what-claude-code-shows.mp4
AI usage disclosure

thumbnails are AI; demos and code are real, running on actual binaries

Fun widget

Read the "Function Hooks: Core Architecture" doc below to figure out what this little animation represents.

spinning 3d onion model

Function Hooks Core Architecture.pdf

Activity

  1. etrippler commented on Sep 3, 2026

    @etrippler

    Looks sick. Ship it IMO.

  2. aviseth commented on Sep 3, 2026

    @aviseth

    This is sick! Can't wait to try it!

  3. AdityaRon commented on Sep 3, 2026

    @AdityaRon

    I run something in this space already — claude-code-harness, security/audit/context hooks built on today's command-hook model (PreToolUse/PostToolUse via settings.json + bash). A few places this architecture would close gaps we've hit in practice:

    Structured args over regex on shell strings. My env-guard/interpreter-guard hooks pattern-match raw Bash strings, with known bypasses we document ourselves (obfuscated payloads, git -c/-C prefixes). A tool.call hook receiving e.tool/e.args as structured fields — matched via { tool: "Bash" } — closes a whole bypass class regex can't.

    Nesting instead of replacement. I ship no custom autoMode rules because setting a category today replaces the shipped defaults rather than adding to them, risking a silent drop of Claude Code's 65 default soft_deny rules. The fold in §2.1 (A∘B∘C∘core, earlier registration wraps and controls more) fixes this structurally — our plugin sits above core and vetoes via before/instead without needing to own a whole ruleset.

    §4.2's capability withholding covers real gaps. gh api is allowlisted with no guard today (unlike kubectl), and MCP connectors are invisible to network-guard entirely since it only sees Bash/WebFetch. A plugin controlling what engine.create forwards — withholding or wrapping the network noun — covers both at the capability level instead of needing a guard per integration.

    Open question: the doc says $ is a plugin's only channel for side effects, with files/network/model/terminal/permissions as the core primitives — MCP isn't named. Do MCP tool calls route through $.tool.call like any other tool (hookable), or reach the model outside $ entirely? That answer determines whether this closes our biggest known gap or leaves it untouched.

  4. DareDev256 commented on Sep 3, 2026

    @DareDev256

    Heavy hook user, so this lands on a real surface: 42 active hooks across 19 events, plus 4 parked.

    Strong yes, and my reason is narrow enough to be worth stating. 35 of those 42 hooks cannot emit a non-zero exit. Failure is unrepresentable in their contract, so nothing downstream can distinguish "ran and worked" from "ran and silently did nothing." I ended up writing a second scheduled job that greps log text and staleness, because exit status certifies nothing. A typed return value fixes that at the type level, and Listing 5 replaces most of what I built to watch my own hooks. Self-observation is exactly what shell hooks are worst at.

    Three things the doc left me unsure of:

    1. Mid-chain throw. §6.2 covers ⊥ throwing. If a hook three deep throws, does the dispatch fail open or closed? For a redaction hook on tool.call, fail-open is strictly worse than having no hook at all, so I'd want that to be a declared property rather than an implementation detail.

    2. Hang budget. next.signal fires when the chain returns or cancels. What cancels a hook whose promise never settles? Is there a per-hook timeout, or does one bad await wedge the event for everyone beneath it?

    3. $ reach. §4 says the environment has no ambient filesystem. Several of my hooks read from ~/.secrets/ and write state outside the project. Does $.fs reach outside the workspace, and is that reach itself matchable so an admin can scope it?

    The $-as-only-door property is the good part: "what a plugin did is exactly the calls it made" is the first plugin model I've seen where an audit log is trustworthy by construction rather than by convention.

  5. wordingone commented on Sep 3, 2026

    @wordingone

    this sounds fantastic. Current hooks are what supplies modern harnesses with most of the control we have, so im excited to see what happens with these function hooks.

    I work with a lot of long running gpu heavy work, so I do think the function hooks could be really helpful with catching errors during/before execution.

    definitely ship it, at least for a little bit, and I'm sure a bunch of users across different platforms will report on the usefullness and use cases (i.e threads, instagram, x.com).

  6. andreimaxim commented on Sep 3, 2026

    @andreimaxim

    It's a great feature, but I'm shocked by extremely poor quality of the presentation, from technical documentation to the other videos. Considering you expect feedback from humans, did any human at Anthropic actually look at the videos?

    For example, I had to watch "Order is nesting" three times to figure out that what it meant and the documentation says "A sequence on(X, A), on(X, B), on(X, C) folds to X = A(B(C(⊥))): plugins registered earlier sit “above” and plugins registered later “beneath” (on ⊥, see §6.2)" and that "earlier registration grants a plugin more authority since it wraps more". Come on, folks! It's just a middleware!

  7. poteat commented on Sep 3, 2026

    @poteat
    ContributorAuthor

    @andreimaxim apologies, indeed I did spend a week copy editing these and running many burns - I'm not satisfied either but I wanted to get this out this week vs. another week of demo polish. It is just a middleware, if you've used Express, I think this will be pretty intuitive.

  8. ahjin2 commented on Sep 3, 2026

    @ahjin2

    Strong yes, and specifically for the capability model rather than the TypeScript ergonomics.

    The property that matters is the one in video ⓸: an admin removes an affordance from $ and nothing registered below can invoke it. That is categorically different from asking a plugin not to do something. In agent-heavy workflows, prohibitions expressed as prose — in a brief, in a rules file, in a system prompt — hold most of the time and fail exactly when the stakes are highest. Mechanical constraints hold. Please keep capability removal front and center if this ships; it is the thing shell hooks structurally cannot provide.

    I say that as someone who runs a production system where most of the code is written by agents, and who deliberately stays in the loop: I confirm actions when agents ask for permission. That posture is exactly what makes the current permission model painful, because it only offers two settings.

    Two gaps this would close:

    1. Scoped self-modification. When an agent needs to edit files under .claude/, the options today are approving interactively every single time, or granting a broad Edit(.claude/**) permission that widens the surface permanently. There is nothing in between. A function hook could express "this path, this shape of edit, nothing else" — so I keep a human gate where it earns its cost and drop it where it doesn't.
    2. Guards that survive delegation. A guard written into a brief does not survive into a sub-agent; it has to be re-stated, and re-stating is where it gets dropped. As a hook wrapping every tool call, the guard exists once and applies to everything below it.
      Two design questions:
    • What happens when a hook throws? Fail-open (the action proceeds) or fail-closed (the action is blocked)? For anything security-shaped, fail-open is a footgun. I'd want it explicit per hook rather than a silent global default.
      • How do I test a hook? Shell hooks today are only ever exercised in production — they escape whatever check suite runs before commit. If function hooks arrive without a test harness, the untested-guardrail problem just moves to a new file format.
        One framing note: every demo presents the admin layer as a team lead constraining developers. For a solo operator the admin is the human and the constrained parties are their own agents — same mechanism, different geometry. If capability removal ends up gated behind enterprise plans, it misses a large share of who needs it.

    Last question: does the existing shell-hook interface stay supported alongside this, or is this a migration?

  9. juanquijandria commented on Sep 3, 2026

    @juanquijandria

    this is good! i can´t wait

  10. DareDev256 commented on Sep 3, 2026

    @DareDev256

    Follow-up with the actual inventory, since the shape of a real surface may be useful for pressure-testing this.

    59 hook registrations across 19 events, from 42 scripts (several register on more than one event):

    event hooks
    PreToolUse 11
    SessionStart 8
    UserPromptSubmit 8
    PostToolUse 7
    SessionEnd 7
    PermissionRequest, Stop 3 each
    13 other events 1 each

    Two things fall out of that.

    Ordering bites in exactly one place. PreToolUse carries 11 hooks and is the only place where "order is nesting" changes behaviour rather than just sequence — a redaction hook and a deny hook disagree about who should see the unredacted command. Today that ordering is implicit array position with no way to express intent. §2.1 fixes it, and it is worth calling out that the fix matters for one event, not nineteen. The long tail of 13 single-hook events is where the typing is the win, not the composition.

    The $-only-door rule is where migration actually gets hard. Classifying my 42 by what they touch:

    • 22 read or write outside the workspace (~/.claude/, state dirs, log files)
    • 9 spawn another process (osascript, launchctl, a preview binary)
    • 6 open a local SQLite database
    • 5 read credentials from ~/.secrets/
    • 3 make network calls

    §4 says the environment has "no ambient file system or network," and that what a plugin did is exactly the calls it made on $. Taken literally, more than half of this surface cannot be expressed as a function hook unless $ grows home-relative fs, subprocess, sockets and sqlite nouns — at which point the auditability claim gets much harder to keep, since $.proc.spawn re-admits everything the door was meant to close.

    That is not an argument against the design. It is the interesting tension in it, and I would rather see it named in the doc than discovered during migration. My guess at the right answer is that these stay command hooks forever and function hooks are for the things commands are bad at — which §1 already permits, but the framing ("a fifth kind") reads like a successor rather than a sibling.

  11. andreimaxim commented on Sep 3, 2026

    @andreimaxim

    @poteat apologies for the harsh tone, it's an awesome feature that Claude Code needs to have.

  12. poteat commented on Sep 3, 2026

    @poteat
    ContributorAuthor

    @DareDev256 to clarify: $.fs, $.http, and $.process will very likely exist. Our prerogative is not to restrict what plugins can do; that's your org admin's job. The point is that there's no ambient support, everything goes through $ so that admins can audit, allowlist, deny, log, etc. any event. We want to support wide use-cases; we're still designing what goes on $ in conjunction with our early access partners.

  13. 275 remaining items

  14. cstillm commented on Oct 21, 2026

    @cstillm

    Feedback from building a small usage-gauge mod for the Desktop App:

    • The prompt-footer slot truncates at about 28 characters, and the space between it and the mode label can't be used. More control over the footer would make it genuinely useful for status mods.
    • Hover reveals don't work in the footer. A card appears only when the line overflows.
    • No accessible labels (screen-reader text) can be attached to footer content, which matters for low-vision users.
    • The band above the prompt always draws as a full-width bar in the same color as the prompt box, even when the content is tiny. Two small rings produced what looks like an empty box stacked on the input. There's no compact or transparent option, so for status it's unusable, and I fell back to footer text.
    • Rate limits reach mods only after the first reply (rateLimits is empty "before the first reading"), though the app's own usage card has the numbers at session start. Exposing them at session start would let status mods be correct immediately.
    Image Image
  15. deefactorial commented on Oct 23, 2026

    @deefactorial

    One request from a plugin that gates merges on subagent review: a verifiable record of a run. Mods expose $.session.id(), $.session.model() and turn.step with the per-request model, which is readable but not provable from outside the process. Filed #104409 asking for a harness-signed run receipt (session id, agent type, model, transcript hash) on SubagentStop / turn.complete; a $.session.receipt() method would be the natural Mods surface for it.

  16. 615morimotorinta-bot commented on Oct 23, 2026

    @615morimotorinta-bot

    Our prompt-footer usage Mod (session / weekly usage, context, cache time left) disappeared from the Desktop App today with no notice.

    • Desktop App, bundled Claude Code 2.1.308, macOS
    • The cached flag tengu_plugin_hooks_modules is false (fetched 2026-10-24 ~07:47 JST)
    • claude plugin test says: "hooks modules are turned off in this process ... if this message returns, installed mods are turned off remotely"
    • claude plugin validate passes, and nothing in the Mod changed

    Two asks:

    1. If this is an intended rollback, please say so here (or in release notes) and give an expected date. Users who rely on a Mod for limits and cache timing lose that information silently.
    2. When Mods are turned off remotely, show a one-line notice in the app (for example where the Mod used to render), so users know it is the switch and not their Mod.
  17. Trollgeir commented on Oct 25, 2026

    @Trollgeir

    I’m building Lacuna, an OpenCode plugin that replaces finished conversation ranges with summaries only in model requests, preserving originals for later restoration.

    session.append cannot support this: a tool result’s usefulness often becomes clear several turns after it arrives.

    The quadratic-work concern is fair. Could mods instead submit projection patches keyed by stable message IDs, avoiding mandatory full-history copying? The engine could validate tool pairing and preserve untouched thinking/media.

    That would enable reversible context compression without changing stored history. This is a concrete use case for #84756.

    This is the only blocker I have currently for making a Claude Code Mod for lacuna.

  18. castus commented on Oct 25, 2026

    @castus

    Pixpal: a pixel-art friend above the prompt that acts out the tool Claude is using, and shows how it feels about the answer.

    Pixpal searching and reading while Claude answers, then reacting to the answer and to a thank-you

    https://github.com/castus/pixpal · MIT · v1.0.0

    • 5 characters (robot, cat, Claude, owl, ghost), a dark and a light look, speech bubbles in 12 built-in languages (and any other by a one-time translation).
    • 15 emotions, picked by keyword rules by default, or by Haiku with /pixpal emotions ai. Nothing leaves the machine unless you turn that on.
    • A mini robot for every running subagent, and it shares the band with other mods.

    Feedback from building it:

    • claude plugin test and $.session.version() made this easy to build and to ship with CI. Thank you.
    • A hooks module can import only its own files and claude-code, so a test cannot read .claude-plugin/plugin.json to check that the version the mod reports (/pixpal report, for bug reports) matches the manifest. I keep the version in a constant and in two JSON files, and bump all three by hand. A way to read the plugin's own manifest, or its version, from $ would remove that.
    • Importing the manifest JSON directly made the module fail to load (13 of my 40 tests stopped loading it), so I fell back to the constant.
    • claude plugin validate warns that a CLAUDE.md at the plugin root is not loaded as plugin context. That is fine for me (it is for contributors to the repo), but a plugin that doubles as a repo hits it by default.
  19. hg913488 commented on Oct 26, 2026

    @hg913488

    Retro Chat: a mod that skins your Claude Code chat as iMessage, MSN Messenger, AIM or IRC. /skin msn and Claude signs in.

    Claude Code chat switching between the iMessage, MSN, AIM and IRC skins

    https://github.com/hg913488/claude-code-retro-skins · MIT

    /plugin install retro-chat --marketplace hg913488/claude-code-retro-skins
    
    • iMessage: blue bubbles, "Delivered", the gray typing bubble
    • MSN: You says:, "Claude is typing a message…", and a popup with the first line of the reply when it's done
    • AIM: timestamps and an away message in the status line ("brb, writing code")
    • IRC: [17:24] <You>, * Claude has joined #demo. No sound. It's IRC.

    Display only: Claude's prompts and answers don't change. Each skin has its own pixel buddy next to the spinner, real pixel art in the desktop app, block characters in the terminal. /skin off undoes it all, and sounds can be switched off in /config. A skin is one file in skins/ if you want to make your own.

  20. wkasekende commented on Oct 26, 2026

    @wkasekende

    Thanks for $.prompt.paint in 2.1.311. We've moved our between-keys repaint onto it, and it works as described.

    One more on the same theme: could a mod style the text cursor without a fill? Something like:

    await $.prompt.paint({ text, decorations, caret: { shape: 'bar' | 'block' | 'underline', color, blink } })

    or a separate $.prompt.caret({ ... }), with the same rules as paint: no text, caret position or undo change, and dropped or kept on the next change, whichever fits the engine. That would let a mod's caret match the rest of its look (theme colours, a high-contrast or larger caret for visibility) without writing to the box.

    Happy to test a build.

  21. wkasekende commented on Oct 26, 2026

    @wkasekende

    Three more from building on $.prompt.paint, shortest first:

    1. Show a character as another, text unchanged. A decoration key like { start, end, replaceWith: '⠋' }: the box draws the replacement over that range, the draft keeps its text. A mod's in-place indicator (a spinner while its work runs) is today a fill per frame, so every frame is a write to the box. With this it would be a paint like everything else.

    2. Feature detection, or optional hooks. A module that hooks an event the engine does not know is refused whole, silently (debug log only), so a mod can't adopt a new event without dropping every older version. Either of these would do:

    on('prompt.change', handler, { optional: true })   // skipped where unknown
    if (await $.engine.has('prompt.paint')) { ... }      // or a capability check

    3. The box's layout. To place virtualText under a span we have to know where the box wraps. Today we read the width from a PromptHint render's viewport.columns, subtract a guessed prefix, and re-implement the word wrap. When the guess is off the text lands in the wrong column. Something like:

    const { columns, rows } = await $.prompt.layout()
    // columns: cells a row of text holds; rows: [{ start, end }] offsets per drawn row; plus the caret's { row, column }

    or the same shape on prompt.edit / prompt.change inputs. Even columns alone, exact, would remove the guess.

    Happy to test any of these.

  22. mesix971 commented on Oct 29, 2026

    @mesix971

    Clawd: a tiny pixel-art Claude who lives above the prompt and silently acts out your session, in 73 animated scenes and no text.

    Clawd moves boxes while Claude works A jack-in-the-box pops out of a box Clawd with a whistle and four mini-Clawds Clawd celebrates a green run

    He carries boxes while Claude works, hammers during builds, throws confetti when tests pass and sits under a rain cloud when they fail. Each subagent arrives as a mini-Clawd in its model's color, hands its work back and leaves in one of five ways. He sweats as the context fills up, wears a visor in auto mode, and levels up from tool calls.

    It ships with atelier, an Android dev band (APK and phone versions, git, quotas, cost, model advice), and Clawd plays gags across it:

    Clawd steals atelier's loading orb, breaks it and glues it back

    https://github.com/mesix971/mods-claude · MIT · unofficial, fan-made

    claude plugin marketplace add mesix971/mods-claude
    claude plugin install clawd@mods-claude
    

    Built only on function hooks (ui.render above the prompt, tool.call, agent.spawn, session.measure), with no network and no model calls.

  23. echelong commented on Oct 30, 2026

    @echelong

    Feature request: race-resistant filesystem capabilities for Claude Mods

    I'm building Cobalt Cockpit, an open-source Claude Code mod that needs to verify worktree state without accessing protected Git administrative content.

    The current Mods filesystem and process APIs appear insufficient for a security-sensitive inventory.

    We've investigated $.fs.list, $.process.run and streaming $.process.spawn, including disposable fixtures with large directories, nested repositories, symlinks, hardlinks and concurrent directory replacement.

    What we found

    • $.fs.list materializes directory entries rather than exposing an incremental cursor.
    • Streaming $.process.spawn improves observed memory behavior, but does not provide object-based filesystem confinement.
    • Path-based .git pruning can be defeated by concurrent directory replacement.
    • Lossy filename decoding, incomplete traversal and subprocess resource enforcement complicate trustworthy results.
    • A native helper does not automatically solve binary integrity or protected-object identification.

    Requested host capabilities

    Would the Mods API consider providing a constrained filesystem interface supporting:

    • Incremental directory cursors with explicit completion.
    • Handle-relative, no-follow child operations.
    • Post-open object identity and link-count metadata.
    • Lossless filename representation.
    • Enforced cancellation and resource budgets.
    • Structured failures for incomplete or unsafe traversal.

    There is also a deeper requirement: protected-object identity.

    Even with handle-relative operations, a protected .git directory renamed before its first observation is indistinguishable from an ordinary directory unless the host provides an independent authority for identifying protected objects.

    We are not asking for arbitrary filesystem access or claiming that openat2 alone solves this.

    The goal is to establish whether the host could expose a narrowly scoped, read-only traversal capability with an explicitly documented trust boundary.

    We have reproducible Linux fixtures, a proposed host capability contract and acceptance tests that can be shared in sanitized form.

    Would this fit the planned Mods API security model, and is there a recommended path for proposing such a capability?

  24. tmoosting commented on Nov 15, 2026

    @tmoosting

    @poteat thanks for agreeing that asUser: true turns should go into the rewind stack (your reply here). Is that still on the list? It's still the case on 2.1.325; #103248 has the details and a ten-line repro.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions