Library / SDK
babashka/sci avatar
babashka/sci

SCI: a small Clojure interpreter for embedded scripting and DSLs

Configurable Clojure/Script interpreter suitable for scripting and Clojure DSLs

1,390 stars107 forksClojureEPL-1.0

At a glance

What is it?
SCI evaluates Clojure source in a sandboxed context that cannot reach the host program, and the same library runs on the JVM, under GraalVM native image, in ClojureScript, in JavaScript and in Dart. The trade-off is a limited dialect with an allow list, a deny list and a namespace whitelist.
Who is it for?
SCI fits teams that need to run user-supplied Clojure, embed a Clojure-shaped DSL in a Java, JavaScript or Dart product, or want one scripting layer shared by a server and a browser client, and it is a better fit than host `eval` precisely because the evaluation context cannot mutate the surrounding program.
Can I use it commercially?
Yes, with conditions. EPL-1.0 is a weak copyleft licence: you can use it inside commercial and closed-source software, but if you distribute changes to its own files, you must publish those changes under the same licence.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly Clojure, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Why eval-string exists: a dialect that cannot reach the host

The stated motivation is short. You want to evaluate code from user input, or you want to use Clojure for a DSL inside your project, but `eval` is not safe or simply does not work. Both halves of that sentence describe real limits: the host's own `eval` runs with the host's powers, and it does not exist in every runtime you might want to ship into.

SCI answers with a small Clojure interpreter whose context is sealed. The central guarantee is stated in the usage section: in SCI, `defn` does not mutate the outside world, only the evaluation context inside a call to `sci/eval-string`. A script can define functions, but those definitions live and die inside the evaluation, which is what makes SCI usable for code that arrives from a user rather than from your own codebase.

The API is correspondingly small. The main function is `sci.core/eval-string`, which takes a string to evaluate and an optional options map, and the full reference lives in `API.md` in the repository. The quickstart shows the shape of it:

clojure
(require '[sci.core :as sci])
(sci/eval-string "(inc 1)") => ;; 2
(sci/eval-string "(inc x)" {:namespaces {'user {'x 2}}}) ;;=> 3

The second call shows the mechanism for giving a script its variables: the options map carries a `:namespaces` entry, and the script sees `x` as if it had been defined in the `user` namespace. Nothing global is touched, and nothing global is needed.

:allow, :deny and :namespaces decide what a script can reach

The options map is the security model, and it has four moving parts.

By default, SCI enables access to most of the Clojure core functions. More can be enabled with `:namespaces` and `:classes`. A list of allowed symbols can be given with `:allow`, and using any other symbol raises an exception. A list of disallowed symbols with `:deny` has the opposite effect.

clojure
user=> (sci/eval-string "(inc 1)" {:allow '[inc]})
2
user=> (sci/eval-string "(dec 1)" {:allow '[inc]})
ExceptionInfo dec is not allowed! [at line 1, column 2]  clojure.core/ex-info (core.clj:4739)
clojure
user=> (sci/eval-string "(inc 1)" {:deny '[inc]})
ExceptionInfo inc is not allowed! [at line 1, column 2]  clojure.core/ex-info (core.clj:4739)

The exception carries the line and column, so a script author gets a position in their own source rather than a generic failure. Note the asymmetry between the two lists: `:allow` is a strict whitelist where anything else is refused, and `:deny` is a blacklist where only the named symbols are refused.

Namespaces work in two directions. You can bind host functions into the script, and you can make a whole namespace requireable from inside a SCI program:

clojure
user=> (def opts {:namespaces {'foo.bar {'println println}}})
user=> (sci/eval-string "(require '[foo.bar :as lib]) (lib/println \"hello\")" opts)
hello
nil

One subtlety the documentation calls out for demonstration purposes: normally you would use SCI's own version of `println`, but the example above deliberately uses Clojure's `println` from `clojure.core` instead, which is why the namespace is bound explicitly.

A first script, a context map, and a symbol list

The quickest useful script needs three things: the library, a string, and a decision about what the string may touch. Here is the sequence the documentation walks through, with the pieces in the order a user meets them.

The first step is a call that binds a host function through a `:namespaces` map and evaluates a string:

clojure
user=> (require '[sci.core :as sci])
user=> (sci/eval-string "(println \"hello\")" {:namespaces {'clojure.core {'println println}}})
hello
nil

The `println` used inside the script is the one bound from the host, which is why the example sets up `'clojure.core {'println println}` before evaluating a string that prints.

The second step is the allow list. Once `:allow` is set, the script is limited to the symbols named in it, and everything else raises an exception with the position of the offending form.

The installation itself is a dependency declaration rather than a script: SCI is published on Clojars as `org.babashka/sci` and used as a dependency. The repository carries both `deps.edn` and `project.clj`, plus `bb.edn` and `shadow-cljs.edn`, which tells you the project itself is built with the Clojure tooling ecosystem rather than with a bespoke build system. The README also points at a browser playground, a Clojure mode running on NextJournal, so the first evaluation can be done without installing anything.

One interpreter across JVM, GraalVM native, ClojureScript, JavaScript and Dart

The list of runtimes SCI works with is the reason it is worth knowing about. It works with Clojure on the JVM, with Clojure compiled with GraalVM native, with ClojureScript even when compiled with `:advanced`, and with JavaScript. It also works with ClojureDart compiled to Dart, and the repository carries a Flutter REPL example under `examples/cljd-flutter-repl`.

That reach is not incidental. A scripting layer that only runs on the JVM is a server-side tool, while a layer that also runs under GraalVM `native-image`, in a browser, and in a mobile app can be the same layer on both sides of a wire. The documentation points at a dedicated GraalVM section for the native-image case.

The repository layout shows the work behind that claim. There is a `libsci/` directory, a `types/` directory for type information, `shadow-cljs.edn` for the ClojureScript build, `min.js.edn` for a minified JavaScript bundle configuration, and `examples/` with `sci`, `sci_repl` and the Flutter example. `bench/` and `reflection.json` sit alongside them, and a `.clj-kondo/` directory means the source is linted with Clj-kondo, which is one of the projects listed as using SCI.

The `CHANGELOG.md` at the root records the chronology, and `SECURITY.md` exists as a separate file, which is a sensible convention for a project whose entire purpose is running code you did not write.

The adopters show two different uses of the same interpreter

The README lists a long roll of projects built on SCI, and the list separates into two quite different patterns of use.

The first pattern is SCI as the scripting layer of a product that accepts user code. Babashka is a Clojure scripting tool that plays well with Bash, nbb is ad-hoc CLJS scripting on Node.js, scittle executes Clojure or ClojureScript directly from browser script tags, Joyride makes VS Code hackable, ClojureBlocks is a visual editor for Clojure, clj-browser-eval turns any HTML input field into a Clojure interpreter, and Obsidian Wielder writes and evaluates Clojure code directly in an Obsidian vault. The same shape appears in Dad, a configuration management tool, and in Cq, a command line data processor for JSON, YAML, EDN, XML and more.

The second pattern is SCI as an implementation detail of a data or validation tool. Malli provides plain data schemas for Clojure and ClojureScript, Datalevin is a Datalog database, Portal is a tool to navigate through your data, and Zprint formats Clojure and ClojureScript code and data. Clj-kondo uses it as a linter, and Overarch is a data driven description of software architecture based on UML and the C4 model. 4clojure, TryClojure.org, Clerk for local-first notebooks, SICMUtils for computer algebra and Maria.cloud for beginners are all running on top of it as well.

The practical point for an evaluator is that SCI has been asked to do both jobs: run arbitrary end-user code with a restricted dialect, and provide a small embedded engine for a specific tool. The second use is the less demanding one, and it is a good first project to try SCI on.

Unrestricted mode is where the defaults are relaxed

The security-relevant part of the design is the set of restrictions that apply by default, and they are stated in the section on unrestricted mode. By default, built-in vars are read-only, and CLJS instance interop is limited to `:classes`. The section then describes the mode that removes those restrictions, and it is the part of the documentation a reviewer should read before exposing SCI to untrusted input.

The distinction matters because the two modes answer different questions. The default mode answers whether a script can be trusted to run without touching the host, and the `:allow`, `:deny`, `:namespaces` and `:classes` options decide its reach. Unrestricted mode answers a different question: what happens when the script is meant to be part of the host rather than a guest in it, for example a REPL that should be able to define things and reach into host objects.

The two rules that make the default safe are both narrow in scope. `defn` does not mutate the outside world, and built-in vars cannot be reassigned. Combined with the allow list, a script that is handed no options gets a Clojure core without a way to reach anything else. The `:classes` key is the deliberate escape hatch for interop, and it is limited by default to the classes you list, so host interop is opt-in and enumerated rather than open.

The `SECURITY.md` file in the repository root is the place to check for the reporting process before shipping a deployment that evaluates untrusted input, and `API.md` is where the exact option semantics live.

EPL-1.0, three recent releases, and a repository that documents itself

The licence is EPL-1.0, with the text in the `LICENSE` file at the root. That matters more than it might look: SCI is a library that other tools embed, and the Eclipse Public Licence is a file-level copyleft, so a project that modifies SCI's source and distributes it has obligations that depend on how the modification is packaged. That is a question for a licence, not for a review, but the fact that the project is EPL-1.0 rather than MIT should be checked before it is vendored into a commercial product.

The project is not archived, and the last push was on 2026-10-15. The recent releases are v0.15.58 and v0.15.57, both on 2026-08-12, and v0.14.56 on 2026-07-14. There is a `CHANGELOG.md` for the chronology, `API.md` for the reference, `doc/` and `dev/` for working material, `script/` for build scripts and `test/` with `test-resources/` for the suite.

A version number that is still below 1.0 is the other signal worth noting. The API is described as configurable and the options map is the contract, so the surface a project depends on is `sci.core/eval-string` plus the option keys, and those are the things to check against a release before upgrading.

Editorial conclusion

SCI fits teams that need to run user-supplied Clojure, embed a Clojure-shaped DSL in a Java, JavaScript or Dart product, or want one scripting layer shared by a server and a browser client, and it is a better fit than host `eval` precisely because the evaluation context cannot mutate the surrounding program. It is the wrong choice for a project that needs the full Clojure compiler, unrestricted interop with every host class, or a language whose grammar is unlike Clojure, because SCI implements a limited dialect whose capabilities are decided by the options map you pass. Before adopting it, read the `API.md` reference for the exact option names, check the `:classes` and `:namespaces` entries your script needs against the documented defaults, and review the `SECURITY.md` file and the unrestricted mode notes, since the default restrictions are what make the sandbox useful.

Frequently asked questions

How do I add SCI to a Clojure project?

SCI is published on Clojars as the dependency `org.babashka/sci`, and the repository ships both `deps.edn` and `project.clj` for consuming it. The main API is `sci.core/eval-string`, which takes a string to evaluate and an optional options map.

How do I limit what a SCI script is allowed to call?

Pass `:allow` with a list of permitted symbols, or `:deny` with a list of forbidden ones. Calling a symbol outside the allow list raises an exception such as `dec is not allowed! [at line 1, column 2]`, and more of the host can be exposed with `:namespaces` and `:classes`.

Can code evaluated by SCI change the host program?

No. In SCI, `defn` does not mutate the outside world, only the evaluation context inside a call to `sci/eval-string`, and by default built-in vars are read-only with CLJS instance interop limited to `:classes`. Unrestricted mode relaxes those defaults deliberately.

Does SCI run in the browser and on native images?

It works with Clojure on the JVM, Clojure compiled with GraalVM native, ClojureScript even when compiled with `:advanced`, JavaScript, and ClojureDart compiled to Dart, with a Flutter REPL example in the repository. scittle, which executes Clojure(Script) directly from browser script tags, is built on it.

Official sources

  1. babashka/sci on GitHub
  2. Issues
  3. License: EPL-1.0
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/babashka-sci.svg)](https://hysenlabs.com/projects/babashka-sci)

Was this useful?