Written in English on purpose, unlike most of
docs/- the in-app failure message points users here, and not all of them read Hebrew.
Short answer: Obsidian 1.12.7. That is the newest version Markport runs, and the pin is deliberate - not a lag in maintenance.
The app will not start. You'll see this instead of the loading spinner:
Obsidian 1.13.4 did not start. This version asks its host for a startup acknowledgement that Markport does not provide. The newest version known to work here is 1.12.7 - see the README.
Nothing is broken on your side and there is nothing to fix. Run
node scripts/update-obsidian-mobile.js --version 1.12.7 to go back.
Obsidian 1.13 added a check at startup. Before it initialises, it asks the host environment for a
terms field, and expects the exact text of an acknowledgement stating that the user may not
distribute the Obsidian application in any form without explicit approval from the Obsidian team.
The channel is platform-dependent, and the acknowledgement sits on both sides - the host supplies it, the renderer compares it against its own copy. Measured 2026-09-05 against both 1.13.4 builds.
| Channel | If it doesn't match | |
|---|---|---|
| Mobile bundle (what Markport runs) | App.getInfo().terms (Capacitor) |
throw new Error |
| Desktop bundle | ipcRenderer.sendSync("terms") (Electron IPC) |
window.close() |
Reconstructed, not copied: the shipped builds are minified, and the mobile one is TypeScript's ES5 downlevel - a generator state machine. Names and shape are ours; the control flow and the channels are theirs. The acknowledgement itself is elided.
The comparing side. Mobile - assets/public/app.js inside Obsidian-1.13.4.apk. App is
Capacitor's App plugin:
const appInfo = await App.getInfo();
Platform.build = appInfo.build;
Platform.version = appInfo.version;
let managedPolicy = "";
try {
managedPolicy = (await App.getManagedPolicy()).value;
} catch {
// swallowed - getManagedPolicy is not fatal
}
if (managedPolicy) freezePolicyOnce(parsePolicy(managedPolicy));
if ("I understand and agree that I am not allowed to … granted by the Obsidian team." !== appInfo.terms) throw new Error();getManagedPolicy() sits inside a try whose catch arm swallows, so it is not fatal. The
terms comparison sits outside it - that is what makes it the only hard blocker.
Desktop renderer, inside obsidian-1.13.4.asar - the same value read over IPC, in the same run
as version and policy:
Platform.version = electron.ipcRenderer.sendSync("version");
Platform.build = electron.remote.app.getVersion();
freezePolicyOnce(electron.ipcRenderer.sendSync("policy"));
const os = requireNode("os");
if (os) {
Platform.deviceName = os.hostname();
Platform.osName = os.version();
Platform.osVersion = os.release();
}
if ("I understand and agree that I am not allowed to … granted by the Obsidian team." !== electron.ipcRenderer.sendSync("terms")) return window.close();The supplying side. The Electron main process, same asar - the constant, and the handler chain it is registered in. This one is quoted close to as-shipped; the desktop build targets a modern Chromium and is not downlevelled:
const ACKNOWLEDGEMENT = "I understand and agree that I am not allowed to … granted by the Obsidian team.";
…
electron.ipcMain.on("terms", e => { e.returnValue = ACKNOWLEDGEMENT }),
electron.ipcMain.on("is-quitting", e => { e.returnValue = isQuitting }),
electron.ipcMain.on("is-closing", e => { const win = electron.BrowserWindow.fromWebContents(e.sender);
e.returnValue = isQuitting || !!(win != null && win.closing) }),
electron.ipcMain.on("desktop-dir", e => { e.returnValue = desktopDir }),
electron.ipcMain.on("documents-dir", e => { e.returnValue = documentsDir }),
electron.ipcMain.on("resources", e => { e.returnValue = resourcesPath }),
electron.ipcMain.on("version", e => { e.returnValue = appVersion }),terms sits between is-quitting and is-closing, answered exactly like version or
resources. On Android the same string is compiled into classes.dex of the 1.13.4 APK (found
with grep -a; not in classes2.dex) and reaches the renderer through Capacitor.
So the acknowledgement is not something the user types or accepts anywhere. It is a constant that ships inside Obsidian's own client, on both halves of it.
On a real Android device, Obsidian's own native layer supplies it. Markport replaces that
native layer with browser shims, and those shims don't supply it - capacitor-shim.js ships
terms empty, and shims/electron.js answers the terms IPC channel with an empty string for
the same reason.
getManagedPolicy() in the excerpt above is not part of the gate, but it is worth knowing about.
It is new in 1.13 - 1.12.7 does not call it at all - and it reads an enterprise MDM
configuration: a JSON string with eight keys.
{ plugins, themes, snippets, sync, publish, webViewer, devTools, insider }Two different parsers. snippets, sync, publish, webViewer, devTools and insider are
strict booleans - only a literal true permits, anything else denies. plugins and themes
accept either true or a non-empty array, which is kept as-is and used as an allow-list of
ids. The default, when no policy is supplied, is everything permitted; the object is
Object.freezed and only the first value wins.
Markport's shims do not implement getManagedPolicy at all. On 1.12.7 that is moot - nothing
calls it. On 1.13+ the call would reject, Obsidian's own try would swallow it, and the app
would run with the permissive default. So an admin-supplied policy would not reach a Markport
instance even if one were configured on the device. Nothing here is a workaround: it is simply
not implemented, and it is not what stops 1.13 from starting.
We could. The shim could return the string and the app would boot.
We don't, because the check isn't a bug or an API change we failed to keep up with - it's a control Obsidian added deliberately, and its content is a statement about redistribution rights. Having this project's code assert that acknowledgement on a user's behalf in order to get past it is not something we're willing to ship. Obsidian is not open source; this is their application, and it's their call to make.
To be explicit about what is in the code: App.getInfo() passes through whatever value the person
running the instance has provided in their own browser, and ships empty. Markport does not
contain the acknowledgement text and does not supply it. Out of the box, on 1.13+, the app does
not start - which is the intended behaviour.
You are an adult with your own copy of Obsidian, and what you do on your own machine is your decision. But be clear with yourself about what that decision is:
Supplying that value is you making the declaration, personally. Not this project, and not whoever wrote the code - you. The acknowledgement is a statement that you understand you may not distribute the Obsidian application in any form without explicit approval from the Obsidian team, and that Obsidian is a registered trademark you may not use without their permission. If you supply it, you are asserting that, and you are responsible for actually complying with it.
Two things follow, and they are not the same:
- Running it privately, for yourself, against your own vault - you are the only person involved, and the declaration is one you can honestly make.
- Serving it to anyone else - a public deployment, a shared link, a hosted instance for a team
- is exactly what the acknowledgement says requires explicit approval. Do not do it on the strength of a flag you set in your own browser.
This project takes no position on your private use and provides no support for it. If it breaks, that's yours too: the pin exists because 1.12.7 is what we test against, and nothing past the gate has been verified on newer versions.
This matters, because "it doesn't work on 1.13" could easily have meant our shim layer had fallen behind. It hasn't. Measured 2026-08-02, comparing 1.12.7 and 1.13.4:
| 1.12.7 | 1.13.4 | |
|---|---|---|
| Boots in the browser | Yes - vault chooser renders fully | No - stops during startup |
app.js runtime errors |
0 | 4 |
native-bridge.js (the Capacitor bridge) |
- | byte-for-byte identical |
| Capacitor plugins referenced | 11 | the same 11 |
| Startup acknowledgement gates | - | exactly one |
| Our test suites | pass | pass |
The download-and-extract pipeline runs cleanly on 1.13.4, the shim contract didn't move, and no
new plugin surface appeared. getManagedPolicy() is new but wrapped in a try/catch inside
Obsidian's own code, so it isn't fatal.
Note this is static analysis plus a boot attempt - it establishes that the acknowledgement gate is the only hard blocker, not that everything past it would behave correctly.
- Obsidian granting explicit permission for this use, which we'd rather ask for than route around.
- A future Obsidian release where the check no longer applies to this scenario.
Until then the pin stands. If you're reading this because you wanted a newer Obsidian in a browser: that's a fair thing to want, and the place to raise it is with the Obsidian team.
AGENTS.md- "Before you touch the bundle", and the zero-patches policysrc/client-mobile/boot.js- the boot watchdog that produces the message above