Skip to content

feat(js): add customBuiltins to Bash and BashTool #1646

Closed

Description

@mayneyao

Python has Bash(custom_builtins={...}) from #1315 but the JS bindings
(@everruns/bashkit) were missing this feature.

What was missing

  • Bash has persistent VFS across execute() calls but no way to
    register JS callbacks as persistent builtins
  • ScriptedTool.addTool() has callbacks but VFS is ephemeral per call
  • The issue described JS bindings already have this via Bash.defineCommand()
    but in reality defineCommand never existed

What was implemented

  • BashOptions.customBuiltins: Record<string, BuiltinCallback> — constructor-time
  • Bash.addBuiltin(name, callback) — post-construction
  • BashTool equivalent support
  • BuiltinContext with name, argv, stdin, env, cwd
  • 10 tests covering pipes, VFS persistence, reset survival, pipelines
import { Bash } from '@everruns/bashkit';

// Needed API that was missing
const bash = new Bash({
  customBuiltins: {
    'get-order': (ctx) =>
      JSON.stringify({ id: ctx.argv[0], status: 'shipped' }) + '\n',
  },
});

await bash.execute('mkdir -p /scratch');
await bash.execute('get-order 42 > /scratch/order.json');     // → VFS
const result = await bash.execute('cat /scratch/order.json');  // persists across calls

Notes

  • executeSync() deadlocks with custom builtins (same limitation as
    ScriptedTool). Use execute() (async).
  • Custom builtins survive reset() but are host-side config — pass
    customBuiltins again on snapshot restore.

Activity

  1. mayneyao commented on May 22, 2026

    @mayneyao
    Author

    I am migrating from just-bash to bashkit. Need persistent VFS + custom builtins together — but Bash/BashTool JS bindings were missing this. Python got it in #1315 (constructor-time custom_builtins dict). JS only had ScriptedTool.addTool() which creates a fresh interpreter per call (no VFS persistence across execute()). So I opened a PR to add customBuiltins + addBuiltin() to the JS bindings.

    @chaliy

  2. chaliy commented on May 24, 2026

    @chaliy
    Contributor

    I went in the little bit other direction, where I added abilty to change builtins in core. I also slop adaopted your changes in terns of contract. - #1721

    Can you check if this wors for you? If yes, I will release a version of the lib with this changes.

    I am also in progress adding couple of perf improvements, and some examples.

  3. mayneyao commented on May 24, 2026

    @mayneyao
    Author

    I went in the little bit other direction, where I added abilty to change builtins in core. I also slop adaopted your changes in terns of contract. - #1721

    Can you check if this wors for you? If yes, I will release a version of the lib with this changes.

    I am also in progress adding couple of perf improvements, and some examples.

    Your BuiltinRegistry approach is much cleaner, thanks for this work!

    Would you consider adding three more features to the JS bindings? All in core already, just need exposure:

    env — BashBuilder::env() → new Bash({ env: { KEY: "val" } })
    network — BashBuilder::network() → new Bash({ network: { allow: ["https://..."] } })
    bash.parse(script) returns AST as plain JS object, agent uses it to inspect/intercept commands before execution

    I've implemented all three on my fork and they work well. Happy to send PRs if you're interested.

  4. chaliy commented on May 24, 2026

    @chaliy
    Contributor

    Your BuiltinRegistry approach is much cleaner, thanks for this work!

    Good. Let me release that.

    env — BashBuilder::env() → new Bash({ env: { KEY: "val" } })

    Yes

    network — BashBuilder::network() → new Bash({ network: { allow: ["https://..."] } })

    Yes

    bash.parse(script) returns AST as plain JS object, agent uses it to inspect/intercept commands before execution

    Yes, but ideally if JSON format would be the same as with rust (e.g. no field renaming)

  5. mayneyao commented on May 27, 2026

    @mayneyao
    Author

    Yes, but ideally if JSON format would be the same as with rust (e.g. no field renaming)

    The original AST is also okay; later, we can expose a user-friendly API that encapsulates a layer. That’s fine.

    I'd like to ask about Python-related support. I noticed that even when I explicitly instruct in the prompt to prohibit the agent from calling Python, most vendors' LLMs still actively use Python to process data. This is likely related to the training data. Python currently lacks interaction with VFS files, and this capability gap means the agent's call requirements cannot be met. For now, Bash can solve most problems, but LLMs still prefer Python. Whether it's Python or TypeScript, they are currently just simple calculation scripts. If they cannot interact with VFS, it's better not to enable them. I'd like to ask about future plans for supporting this aspect.

  6. mayneyao commented on May 27, 2026

    @mayneyao
    Author

    In most file-related scenarios, LLMs use the with open('/tmp/xxx') syntax, which is currently not supported. This leads to many erroneous calls, and using prompts that limit the Python environment cannot effectively mitigate this.

  7. chaliy commented on May 28, 2026

    @chaliy
    Contributor

    I'd like to ask about Python-related support. I noticed that even when I explicitly instruct in the prompt to prohibit the agent from calling Python, most vendors' LLMs still actively use Python to process data. This is likely related to the training data. Python currently lacks interaction with VFS files, and this capability gap means the agent's call requirements cannot be met. For now, Bash can solve most problems, but LLMs still prefer Python. Whether it's Python or TypeScript, they are currently just simple calculation scripts. If they cannot interact with VFS, it's better not to enable them. I'd like to ask about future plans for supporting this aspect.

    My current solution, is not opt-in python in bashkit for cases when Agent does not really need it.

    Root case of this, we use in-memory implementation of Python - Monty. And they choose not to support open. It supports pathlib.Path, so with more stricted prompting it can work, pathlib.Path supports VFS.

    I was thinking about fork of Monty to add FS and network abstractions, but for now this out of my focus.

  8. chaliy commented on May 28, 2026

    @chaliy
    Contributor

    Oh! Nice. Actually in latest not yet released version of Monty, there is an extensibility point for Open. Nice-nice. Let's me wire this :).

  9. chaliy commented on May 28, 2026

    @chaliy
    Contributor

    I merged #1800 , but tommorrow I will need to built more testing around this. If you have some time to check if this works, that would be awesome.

    (But also, Monty is under active development, and bashkit uses latest main commit, this not more then experimental, pls be careful)

  10. mayneyao commented on May 28, 2026

    @mayneyao
    Author

    That's so fast! Cool, I'll test it.

  11. chaliy commented on Jun 1, 2026

    @chaliy
    Contributor

    Hey, if you do mind, I will close this issue. Feel free to reopen or submit new one at any time.

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