Skip to content

[Feature request] Adding hooks such as mount and env to Extensions, and expose to the JavaScript API #2291

Closed as not planned

Description

@Niapya

Background

While Rust has the Extension trait, the JavaScript @everruns/bashkit library lacks this capability, ScriptedTool cannot be registered as a Bash instance.

While the API does provide capabilities for mounting file systems and loading environment variables (via the files, mounts, and env options), these are defined within BashOptions and can only be passed once during construction. As a result, they cannot be broken down into a reusable, publishable extension module.

Suggestion

  1. Expose a TypeScript extension API in @everruns/bashkit.
const bash = await Bash.create({ extensions: [skill] });
bash.installExtension(skill);
  1. The extension is an object containing a set of hooks responsible for mounting the file system, loading environment variables, registering commands, and other operations. Bash calls these hooks when the extension is created and each time it is executed.
class SkillExtension {
  // Initialization hook: mount the file system, load environment variables, and register commands
  onInit(ctx: ExtensionInitContext) {
    ctx.mount(“/skills/my-skill”, skillData.fs);   // Mount the file system
    ctx.env(“SKILL_PATH”, “/skills/my-skill”);     // Load environment variables
    ctx.command(...);
  }

  // Lifecycle hook: Before each `execute` begins
  onBeforeExecute(ctx: ExecContext) {
    // Optional preparation before each execution
  }
}

Expectations

Users should be able to create a skill extension that mounts an external S3 bucket into a virtual Bash environment during initialization, stores the mount path as an environment variable, and then creates a new command skill. This extension can be published for others to use.

Activity

  1. chaliy commented on Aug 13, 2026

    @chaliy
    Contributor

    Thanks for the detailed writeup — the gap you identified was real, and a first piece of it just landed on main in #2292 (not yet released; it'll be in the next version).

    What's available now

    env was the missing half. mount was already callable on a live instance, but env was construction-only, so an extension couldn't be a single function — it had to be a config fragment the host merged by hand plus a post-construction apply step. There is now setEnv (JS) / set_env (Python), and both bindings replay host setEnv/mount calls across reset().

    So your SkillExtension is now ordinary userland code — no new framework needed:

    import { Bash, FileSystem } from "@everruns/bashkit";
    
    const SKILL_PATH = "/skills/my-skill";
    
    // A publishable module: one function, one instance.
    export function installSkill(bash: Bash, skillData: FileSystem): void {
      bash.mount(SKILL_PATH, skillData);              // mount the filesystem
      bash.setEnv("SKILL_PATH", SKILL_PATH);          // load the env var
      bash.addBuiltin("my-skill", (ctx) =>            // register the command
        ctx.fs.readFile(`${SKILL_PATH}/SKILL.md`),
      );
    }
    const bash = new Bash();
    installSkill(bash, skillData);
    
    await bash.execute('my-skill; cat "$SKILL_PATH/SKILL.md"');

    And composing several is just a loop, which is what extensions: [...] would have desugared to anyway:

    for (const install of [installSkill, installOtherThing]) install(bash);

    The lifecycle part matters as much as the ergonomics. Previously reset() kept the builtin but dropped the mount and had no env to keep, so an extension came back half-installed. Now all three survive:

    fresh:               "# my-skill\n/skills/my-skill\n"
    after reset:         "# my-skill\n# my-skill\n"
    after unmount+reset: 1                              # unmount retracts; reset doesn't resurrect
    

    Env that a script exported is still discarded by reset() — only host-set values are replayed.

    Python is identical (bash.set_env(...), bash.mount(...), bash.add_builtin(...)).

    What this deliberately doesn't do

    Three parts of your proposal are still open, and I'd like your read on whether they're needed for your use case:

    1. A declarative extensions: [...] / installExtension API. With the above, this is sugar over a for loop. Worth adding if extensions need identity — a name, a version, duplicate detection, ordering guarantees — but not otherwise.
    2. Schema-backed ctx.command(). addBuiltin hands the callback raw argv. Flag parsing, --dry-run, help, and discover currently exist only in ScriptedTool, which is a separate class with no mount. If your skill commands want LLM-facing schemas and discovery, that gap is the real one, and it's independent of the extension packaging.
    3. onBeforeExecute. I left this out on purpose: it puts a JS callback on the hot path, and it can't run under executeSync at all (the sync path can't call back into JS). What did you want it for? If it's re-applying state per execution, the replay behavior above may already cover it; if it's per-execution auth or context, that's a different design.

    On the S3 example

    Worth flagging since it shapes the answer: the Node binding has no JS-implemented filesystem interface. mount takes either a host directory (allowlist-gated) or an in-memory FileSystem, so "mount an S3 bucket" today means fetching objects into a FileSystem eagerly rather than lazily on read. If lazy/streaming access to a remote bucket is what you actually need, that's a separate feature (a JS-side FileSystem interface) and worth its own issue — let us know and we'll track it.

    Does the above cover your use case, or is one of (1)-(3) blocking for you?

  2. Niapya commented on Aug 13, 2026

    @Niapya
    Author

    Thanks for the reply. After reading more of the BashKit source, I noticed a related SDK capability gap.

    ScriptedTool is available in the Rust, Python, and TypeScript SDKs. However, in the TypeScript SDK, ScriptedTool can only be created and executed as a standalone tool; it cannot be registered into an existing Bash instance.

    Currently, TypeScript Bash.addBuiltin() only accepts callbacks that receive raw argv. It does not provide the schema, help, discover, or dry-run behavior available for commands registered through ScriptedTool.

    I also noticed that the Rust core already has ToolDefExtension, which can install a group of ToolDef/callback-backed commands into a regular Bash instance. This capability does not appear to be exposed through the Python and TypeScript bindings yet.

  3. Niapya commented on Aug 13, 2026

    @Niapya
    Author

    Regarding the Extension itself, I think BashKit could provide a unified and reusable plugin abstraction on top of these lower-level capabilities.

    A standalone installer function is workable for a single integration, but a first-class Extension API could provide npm packages with a consistent shape, identity, configuration model, lifecycle, composition model, command-conflict handling, and uninstall semantics. This would allow third parties to package their services as BashKit extensions, publish them to npm, and let other applications install and use them directly.

    For example:

    1. A Agent Skills Extension could configure a skill source, set related environment variables, and register commands such as skills add.
    2. A Agent Memory Extension could integrate an external memory or database service and register commands such as memory list

    I am not asking the Extension API itself to immediately support lazy S3 or remote-database filesystem access. The current JS binding does not appear to expose an interface that allows JavaScript to implement a FileSystem.

    My proposed Extension API would be the standard packaging and lifecycle layer above the existing runtime primitives, so third-party integrations would not need to reinvent installation, naming, configuration, ordering, and cleanup conventions for every host application.

  4. chaliy commented on Aug 13, 2026

    @chaliy
    Contributor

    I am not sure about Extension API. Baskit is more an lower level runtime. If your goal to have extensible system, maybe you can builad a package that would add extension system.

    Also about scripted_tool, I am actually thinking to remove it from core Bashkit. scripted_tool is too opionated, and Bahskit should be more like enabler for different versions of codemode implementations. I would probably extract it to own package and maintain externally.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions