(Wasm should be pronounced like awesome starting with a W — as stated in the WebAssembly Music talk at WebAssembly Summit 2020).
Git for Node.js and the browser, using libgit2 compiled to WebAssembly with Emscripten.
Imagine that every app you use kept a complete journal of your data: every change recorded, a backup on your own computer that is truly yours, and synchronization between your devices whenever you want it. Software developers have had exactly this for decades — the tool is called Git. Wasm-git brings that same engine to web applications: store user data locally in the browser with full change history, and push/pull to a remote when — and only when — the user chooses. The application doesn't need to expose any Git terminology: a single "synchronize data" button can do all of it behind the scenes.
npm install wasm-gitThe package ships prebuilt WebAssembly binaries in several variants (see Choosing a variant below). The easiest one to start with is the async variant, which runs on the browser main thread or in Node.js with plain async/await:
import initGit from 'wasm-git/lg2_async.js';
const lg = await initGit();
const FS = lg.FS;
// Git needs to know who you are before committing
FS.writeFile('/home/web_user/.gitconfig',
'[user]\nname = Your Name\nemail = you@example.com');
// Create a repository, add a file, and commit
FS.mkdir('/myrepo');
FS.chdir('/myrepo');
await lg.callMain(['init', '.']);
FS.writeFile('data.json', JSON.stringify({ hello: 'world' }));
await lg.callMain(['add', 'data.json']);
await lg.callMain(['commit', '-m', 'first commit']);Cloning and pushing over HTTP works the same way:
await lg.callMain(['clone', 'https://your-git-server.example.com/repo.git', 'repo']);
FS.chdir('repo');
// ...edit files, add, commit...
await lg.callMain(['push']);When cloning from the browser, the git server must allow cross-origin requests (CORS) — or be served from the same origin. githttpserver is a ready-made reference server, and its live instance at https://wasm-git.petersalomonsen.com/ is an open playground where you can try cloning, editing, committing and pushing directly in the browser (please don't abuse it — it's there for you to test).
The files can also be loaded from public CDNs such as unpkg or jsDelivr (e.g. https://unpkg.com/wasm-git/lg2_async.js).
| Variant | File | Where it runs | Persistence | Notes |
|---|---|---|---|---|
| Sync | lg2.js |
Browser: Web Worker only. Node.js: main thread or worker_threads | MEMFS / IDBFS / NODEFS | Smallest binary; synchronous callMain. Needs a worker in the browser because of synchronous HTTP and long-running operations. |
| Async | lg2_async.js |
Browser main thread or worker; Node.js | MEMFS / IDBFS / NODEFS | Asyncify build: await lg.callMain(...). Larger binary, simplest client code. |
| OPFS (pthreads) | lg2_opfs.js |
Web Worker, requires cross-origin isolation | OPFS | Fastest OPFS variant; needs COOP/COEP headers for SharedArrayBuffer. |
| OPFS (JSPI) | lg2_opfs_jspi.js |
Web Worker, no isolation needed | OPFS | SAB-free, smallest OPFS binary; needs a JSPI-capable browser. |
| OPFS (ASYNCIFY) | lg2_opfs_async.js |
Web Worker, no isolation needed | OPFS | SAB-free universal fallback; largest binary. |
| OPFS auto-loader | lg2_opfs_auto.js |
Web Worker | OPFS | Picks the best supported OPFS build at runtime and exposes a uniform API — see OPFS variants and the runtime loader. |
For browser persistence with the sync/async variants, mount IDBFS and call FS.syncfs — or use the OPFS variants, which persist through the filesystem itself with better performance and quota management.
Complete working examples for every variant are in the test folders:
- test for Node.js
- test-browser for the sync version in a web worker
- test-browser-async for the async version
- test-browser-opfs for the OPFS (pthreads/WASMFS) version
- test-browser-opfs-noniso for the SAB-free OPFS variants (ASYNCIFY + JSPI) and the runtime loader
- Ariz-Portfolio — tracks crypto asset balances, profits and losses on the NEAR protocol blockchain. Uses wasm-git to store portfolio data with full change history on the user's own machine, and lets the user download a complete copy — history included — to their computer. Its optional cloud sync is end-to-end encrypted with encrypted-git-storage: repositories are encrypted before upload with keys only the user holds.
- Y42 — a data pipeline platform (DataOps) where every integration, model and dashboard is versioned in git. In the Data Council talk Using GIT as a NoSQL Database, Y42's founder Hung Dang explains that "under the hood we developed our own git client for the browser using wasm-git, which is a WebAssembly compiled version of libgit2" — and recommends checking out wasm-git at the end of the talk.
- WebAssembly Music — a music-making environment in the browser and wasm-git's original driving use case: compositions and instruments are stored and versioned locally, and synchronized with a remote when the user chooses. See it in action in this video playlist.
- AutoDev — an AI-native multi-agent development platform built on Kotlin Multiplatform: wasm-git is the git backend for its WebAssembly target, powering clone, log, diff and status in
GitOperations.wasmJs.kt. - githttpserver — a CORS-enabled git server you can host yourself, plus the browser playground at https://wasm-git.petersalomonsen.com/ showing basic operations: clone, edit, add, commit, push and pull.
- encrypted-git-storage — an addon that encrypts and decrypts repositories locally in the browser (AES-256-GCM); the server only ever sees ciphertext. Includes a remote helper for the native git client, so users can clone their own application data — decrypted, with full history — straight to their computer.
Wasm-git is also covered in the book Building and Deploying WebAssembly Apps (BPB Publications, 2025) by wasm-git author Peter Salomonsen.
The pthreads OPFS version must run in a Web Worker because it requires SharedArrayBuffer (for pthreads), which in turn requires Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp (or credentialless) HTTP headers.
worker.js (Web Worker):
// Import the OPFS-enabled wasm-git module
const lgMod = await import('./lg2_opfs.js');
const lg = await lgMod.default();
const FS = lg.FS;
// WASMFS doesn't pre-create /home/web_user like MEMFS
try { FS.mkdir('/home'); } catch(e) {}
try { FS.mkdir('/home/web_user'); } catch(e) {}
FS.writeFile('/home/web_user/.gitconfig',
'[user]\n name = Your Name\n email = your.email@example.com');
// Create an OPFS-backed directory using the WASMFS OPFS backend
const backend = lg._lg2_create_opfs_backend();
const workingDir = '/opfs';
// Use ccall to marshal the JS string to a C pointer
lg.ccall('lg2_create_directory', 'number', ['string', 'number', 'number'],
[workingDir, 0o777, backend]);
// Clone using absolute path to avoid CWD ambiguity with WASMFS
const repoDir = workingDir + '/myrepo';
lg.callMain(['clone', 'https://github.com/user/repo.git', repoDir]);
// Work around a WASMFS getcwd() bug: create a root symlink so the path
// returned by getcwd() resolves correctly for libgit2's repo discovery.
FS.symlink(repoDir, '/myrepo');
FS.chdir(repoDir);
// Re-set CWD before each callMain since WASMFS may reset it
FS.chdir(repoDir);
FS.writeFile('newfile.txt', 'Hello OPFS!');
FS.chdir(repoDir);
lg.callMain(['add', 'newfile.txt']);
FS.chdir(repoDir);
lg.callMain(['commit', '-m', 'Add new file']);
FS.chdir(repoDir);
lg.callMain(['push']);
postMessage({ done: true });See test-browser-opfs/worker.js for a complete working example.
OPFS's open/dir/metadata operations (getFileHandle, getDirectoryHandle,
removeEntry, entries, createSyncAccessHandle) are async-only. To call
them from libgit2's synchronous file IO, wasm-git ships three OPFS builds that
bridge async→sync in different ways:
| Build | File | Bridge | SharedArrayBuffer / cross-origin isolation | wasm size | Relative speed |
|---|---|---|---|---|---|
| pthreads / WASMFS | lg2_opfs.js |
WASMFS OPFS backend blocks a worker thread via Atomics.wait |
Required (COOP/COEP) | ~920 KB | fastest |
| JSPI | lg2_opfs_jspi.js |
JSPI — native WebAssembly stack switching | Not required | ~805 KB | fast |
| ASYNCIFY | lg2_opfs_async.js |
Asyncify — wasm rewritten to unwind/rewind its stack | Not required | ~1.5 MB | slower (instrumentation overhead) |
(wasm sizes are for -O3 release builds and will drift; JSPI is the smallest
because it needs no stack-rewriting instrumentation, ASYNCIFY the largest for the
same reason.)
The pthreads build needs Cross-Origin-Opener-Policy / Cross-Origin-Embedder-Policy
headers (cross-origin isolation), which some hosts (e.g. NEAR web4) cannot set.
The JSPI and ASYNCIFY builds are SAB-free: they persist to OPFS by
suspending the wasm stack across the async OPFS calls, so they run with
self.crossOriginIsolated === false. All three must run inside a Web Worker
(OPFS sync access handles require one).
lg2_opfs_auto.js picks the most optimal supported build at runtime and exposes
a uniform git API over all three. Selection order:
- pthreads when the page is cross-origin isolated
(
crossOriginIsolated === trueandSharedArrayBufferexists); - JSPI when available (
WebAssembly.Suspending/WebAssembly.promising); - ASYNCIFY otherwise (universal fallback).
If OPFS itself is unavailable (navigator.storage.getDirectory missing, insecure
context), selectOpfsVariant() returns null — fall back to the non-OPFS IDBFS
build (lg2.js).
// inside a Web Worker (type: 'module')
import { loadOpfsGit, selectOpfsVariant } from './lg2_opfs_auto.js';
console.log(selectOpfsVariant()); // 'pthreads' | 'jspi' | 'asyncify' | null
const git = await loadOpfsGit({ user: 'Your Name', email: 'you@example.com' });
console.log(git.variant); // which build was loaded
await git.clone('https://example.com/repo.git', 'repo.git');
await git.writeFile('repo.git', 'a.txt', 'hello');
await git.addCommitPush('repo.git', 'a.txt', 'add a.txt');
console.log(git.readFile('repo.git', 'a.txt')); // 'hello'
// After a reload, restore an existing repo from OPFS before using it:
await git.syncRepo('repo.git');The detection helpers (detectOpfsEnvironment, selectOpfsVariant) are pure
functions that accept an env object, so they can be unit-tested or forced
(see test/opfs-detect.spec.js).
See test-browser-opfs-noniso/worker.js for a complete worker built on the loader.
Wasm-git supports multiple filesystem backends for different use cases:
- Use case: In-memory storage, not persisted
- Build target: Default (
./build.sh Release) - Browser support: All browsers
- Use case: Browser persistent storage using IndexedDB
- Build target: Default (
./build.sh Release) - Browser support: All browsers with IndexedDB
- Use case: Node.js native filesystem access
- Build target: Default (
./build.sh Release) - Platform: Node.js only
- Use case: Modern browser persistent storage with better performance and quota management
- Browser support: Chrome 86+, Edge 86+, Firefox 111+, Safari 15.2+ (JSPI variant: Chromium-based browsers with JSPI)
- Advantages: Better performance and quota compared to IDBFS
- Requirements: Must run in a Web Worker (OPFS sync access handles require one)
- Three variants — see OPFS variants and the runtime loader:
- pthreads / WASMFS (
./build.sh Release-opfs→lg2_opfs.js): synchronouscallMain; requires cross-origin isolation (Cross-Origin-Opener-Policy: same-origin+Cross-Origin-Embedder-Policy: require-corp/credentialless) for SharedArrayBuffer. - JSPI (
./build.sh Release-opfs-jspi→lg2_opfs_jspi.js): SAB-free; asynccallMain; smallest binary. - ASYNCIFY (
./build.sh Release-opfs-async→lg2_opfs_async.js): SAB-free; asynccallMain; universal fallback, largest binary. lg2_opfs_auto.jsselects the best supported variant at runtime.
- pthreads / WASMFS (
- libgit2: v1.9.4
- Emscripten: Pinned to 6.0.3
- Node.js: v18+
- Browsers: Modern browsers with WebAssembly support
- Emscripten SDK (version 6.0.3)
- Node.js (v18 or higher)
- CMake
- Make
-
Clone the repository
git clone https://github.com/petersalomonsen/wasm-git.git cd wasm-git -
Install and activate Emscripten
git clone https://github.com/emscripten-core/emsdk.git cd emsdk ./emsdk install 6.0.3 ./emsdk activate 6.0.3 source ./emsdk_env.sh cd ..
-
Set up libgit2
./setup.sh
This script downloads libgit2 v1.9.4 and applies necessary patches for WebAssembly compilation.
-
Build the project
cd emscriptenbuild ./build.sh # Debug build (smaller, for development) ./build.sh Release # Release build (optimized)
For async versions (with Asyncify support):
./build.sh Debug-async # Debug async build ./build.sh Release-async # Release async build
For OPFS versions:
./build.sh Release-opfs # pthreads / WASMFS OPFS build (needs COOP/COEP) ./build.sh Release-opfs-jspi # SAB-free OPFS build using JSPI ./build.sh Release-opfs-async # SAB-free OPFS build using ASYNCIFY # (Debug-opfs, Debug-opfs-jspi and Debug-opfs-async also exist)
-
Install npm dependencies
npm install
-
Run tests
npm test # Run Node.js tests npm run test-browser # Run browser tests (sync version) npm run test-browser-async # Run browser tests (async version) npm run test-browser-opfs # OPFS pthreads/WASMFS tests (cross-origin isolated) npm run test-browser-opfs-noniso # SAB-free OPFS tests (ASYNCIFY + JSPI, non-isolated) npm run test-opfs-detect # Loader variant-detection unit tests npm run test-opfs-loader # Multi-browser loader selection tests (Chromium/Firefox/WebKit)
The easiest way to get started is using GitHub Codespaces. The repository includes a .devcontainer configuration that automatically sets up the complete development environment with all dependencies.
The Github actions test pipeline shows all the commands needed for CI/CD and can be used as a reference for local setup.
After building, you'll find the following files in emscriptenbuild/libgit2/examples/:
lg2.jsandlg2.wasm- Synchronous versionlg2_async.jsandlg2_async.wasm- Asynchronous version with Asyncifylg2_opfs.jsandlg2_opfs.wasm- OPFS version with WASMFS + pthreadslg2_opfs_jspi.jsandlg2_opfs_jspi.wasm- SAB-free OPFS version using JSPIlg2_opfs_async.jsandlg2_opfs_async.wasm- SAB-free OPFS version using ASYNCIFY
The runtime loader lg2_opfs_auto.js (a hand-written module, not a build output)
selects the best of the three OPFS builds at runtime — see
OPFS variants and the runtime loader.
These files are also available from npm packages and CDNs for production use.
-
Build errors: Ensure the Emscripten environment is properly activated:
source /path/to/emsdk/emsdk_env.sh -
Test failures: Remove any stale test directories before running tests:
rm -rf nodefsclonetest npm test
Wasm-git started in 2020, and getting libgit2 to run in a JavaScript environment required several fixes to Emscripten itself, all merged upstream that year: #10095, #10526, #10782 and #10669 (NODEFS support).