SiYuan is a personal knowledge base that treats everything as blocks: any paragraph, heading or list item can be referenced, embedded, linked both ways and queried with SQL. The editor is WYSIWYG Markdown with outlines, maths, charts, flowcharts and Gantt charts, and around it sit PDF annotation with links, web clipping, flashcards with spaced repetition, a database table view, OCR, and AI writing and Q&A through an OpenAI-compatible API.
It runs on Windows, macOS, Linux, Android, iOS and HarmonyOS, or as a Docker container you open in a browser, and a built-in command-line tool can search and export a workspace directly. Data lives in a workspace folder as JSON .sy files plus your assets. It is AGPL-3.0, with a Go kernel and a TypeScript interface, and has about 47,000 stars, at v3.8.8 when scanned.
It is freemium: most features are free, even for commercial use, but some, including connecting third-party cloud storage for sync, need a paid membership. Do not keep the workspace in a Dropbox-style sync folder, which the README warns can corrupt data, and note that the Docker version is browser-only and cannot export to PDF, HTML or Word.
- Repository: github.com/siyuan-note/siyuan
- Licence: AGPL-3.0 (GNU Affero General Public License v3.0)
- Language: TypeScript. Stars: 46.9K. Forks: 3,090. Last push: Oct 26, 2026.
- Scan: safe, Oct 26, 2026, commit bfe3610
Who it is for
Note-takers who want Roam or Notion-style linked blocks without handing their notes to a cloud service, and home-lab users who want a notes app on their own server.
Getting started
1. Self-host with Docker (replace xxx with a strong lock-screen password; it protects your notes)
docker run -d -v /siyuan/workspace:/siyuan/workspace -p 6806:6806 b3log/siyuan serve --workspace=/siyuan/workspace/ --accessAuthCode=xxx2. Search a workspace from the built-in CLI
siyuan search "keyword" -w ~/SiYuan -f jsonThen open http://localhost:6806. For the desktop and mobile apps, the project recommends the App Store, Google Play, F-Droid or the Microsoft Store so updates are one click; installers are also on the GitHub releases page. For remote access, put the container behind an HTTPS reverse proxy; OIDC sign-in can replace the access code.
Safety scan
We cloned siyuan-note/siyuan at commit bfe3610 on Oct 26, 2026 and ran the checks described on the GitHub Tools page: credential patterns, decode-and-execute code, install-time scripts, committed binaries, risky CI workflows, every host the code talks to, known vulnerabilities in pinned dependencies, and project hygiene. A person read every hit. This is what we found.
- The 3 secret hits are the text -----BEGIN PRIVATE KEY----- used as a marker, in kernel/bazaar/plugin_project.go and two test files; there is no real key. The 16 bare-IP URLs are all in Go tests, using 8.8.8.8 and the reserved documentation ranges 192.0.2.1 and 203.0.113.10 to test the web-fetch tool and login sessions.
- The 2 pattern hits are long lines in bundled front-end code: highlight.js language definitions and the editor's own protyle-html.js.
- 3 committed binaries: app/elevator/elevator-amd64.exe and elevator-arm64.exe, a small Windows helper for actions that need elevated rights, built from the project's own siyuan-note/elevator repository (the folder's README links to it), and nsProcess.dll, a standard plugin for the NSIS Windows installer.
- 113 known advisories: 89 in app/pnpm-lock.yaml (1 critical, a node-tar decompression issue) and 24 in kernel/go.mod (9 high). The npm ones are dominated by XML and archive parsers such as xmldom and node-tar, typical of Electron packaging tools.
- 7 workflows. One uses pull_request_target (target-branch.yml), and it only moves new pull requests to the dev branch and leaves a comment; it does not check out the submitted code. None of 16 third-party actions is pinned to a commit. Security policy, Dependabot, contributing guide and code of conduct present. OpenSSF Scorecard 5 of 10.
What the scanner counted
| Check | Result |
|---|---|
| Secrets | 3 candidates found and read; see the notes above. |
| Suspicious code | 2 pattern hits found and read; every one is listed under the raw findings. |
| Install-time code | None: nothing runs at install beyond the package manager itself. |
| Committed binaries | 3 executable or compiled objects committed; listed under the raw findings. |
| CI workflows | 7 workflows. 1 uses pull_request_target, none check out the pull request head. 16 of 16 third-party actions pinned to a tag rather than a commit. |
| Network hosts | 40 distinct hosts referenced from source; most often github.com, www.gnu.org, ld246.com, www.apache.org. 16 URLs to a bare IP address, listed under the raw findings. |
| Known vulnerabilities | 113 advisories across 938 pinned packages: 1 critical, 74 high, 26 moderate, 4 low, 8 unrated. app/pnpm-lock.yaml: 708 packages, 89 advisories; kernel/go.mod: 230 packages, 24 advisories. |
| Project hygiene | Has security policy, automated dependency updates, licence file, contributing guide. Missing CodeQL. |
| OpenSSF Scorecard | 5.0 out of 10, as of Oct 19, 2026. |
The raw findings
Every hit the scanner wrote out, with a link to the exact line at the scanned commit. Secrets candidates are redacted.
Secret candidates (3, redacted)
| Where | Rule | Match |
|---|---|---|
| kernel/bazaar/plugin_project.go:70 | private-key | -----B…--- (27 chars) |
| kernel/bazaar/plugin_project_test.go:123 | private-key | -----B…--- (35 chars) |
| kernel/util/path_guard_test.go:155 | private-key | -----B…--- (30 chars) |
Pattern hits (2)
| Where | Rule | Match |
|---|---|---|
| app/stage/protyle/js/highlight.js/third-languages.js:113 | very-long-line | 4154 chars |
| app/stage/protyle/js/protyle-html.js:2 | very-long-line | 23035 chars |
URLs to bare IP addresses (16)
| Where | Rule | Match |
|---|---|---|
| kernel/mcp/tools/web_test.go:33 | ip-literal-url | fetch, _ := webFetchHandler(context.Background(), map[string]any{"url": "https://8.8.8.8"}) |
| kernel/mcp/tools/web_test.go:39 | ip-literal-url | fetch, _ = webFetchHandler(context.Background(), map[string]any{"url": "https://8.8.8.8"}) |
| kernel/model/session_test.go:276 | ip-literal-url | loginRequest := httptest.NewRequest(http.MethodGet, "http://192.0.2.1:6806/login", nil) |
| kernel/model/session_test.go:285 | ip-literal-url | request := httptest.NewRequest(http.MethodPost, "http://192.0.2.1:6806/api/notebook/lsNotebooks", nil) |
| kernel/model/session_test.go:296 | ip-literal-url | if recorder := request("http://192.0.2.1:6806"); recorder.Code != http.StatusNoContent { |
| kernel/model/session_test.go:438 | ip-literal-url | {name: "authenticated same-origin API", method: http.MethodPost, path: "/api/test", origin: "http://192.0.2.1:6806", site: "same-origin", authenticated: true, w… |
| kernel/util/deepseek_web_test.go:105 | ip-literal-url | if request.URL.Path != "/v1/tools/fetch" || len(payload) != 1 || payload["url"] != "https://8.8.8.8/page" { |
| kernel/util/deepseek_web_test.go:109 | ip-literal-url | text, err := DeepSeekWebFetch(context.Background(), DeepSeekWebOptions{APIKey: "test-key"}, "https://8.8.8.8/page", format) |
| kernel/util/deepseek_web_test.go:124 | ip-literal-url | if _, err := DeepSeekWebFetch(context.Background(), DeepSeekWebOptions{APIKey: "test-key"}, "https://8.8.8.8", "markdown"); err == nil { |
| kernel/util/deepseek_web_test.go:174 | ip-literal-url | markdown, err := DeepSeekWebFetch(context.Background(), DeepSeekWebOptions{APIKey: "test-key"}, "https://8.8.8.8", "markdown") |
| kernel/util/deepseek_web_test.go:178 | ip-literal-url | text, err := DeepSeekWebFetch(context.Background(), DeepSeekWebOptions{APIKey: "test-key"}, "https://8.8.8.8", "text") |
| kernel/util/deepseek_web_test.go:195 | ip-literal-url | text, err := DeepSeekWebFetch(context.Background(), DeepSeekWebOptions{APIKey: "test-key"}, "https://8.8.8.8", "text") |
| kernel/util/web_timeout_test.go:55 | ip-literal-url | if _, err := WebFetchContext(ctx, "https://8.8.8.8", "markdown", input); err != nil { |
| kernel/util/web_timeout_test.go:64 | ip-literal-url | if _, err := DeepSeekWebFetch(ctx, options, "https://8.8.8.8", "markdown"); err != nil { |
| kernel/util/web_timeout_test.go:93 | ip-literal-url | _, err = WebFetchContext(ctx, "https://8.8.8.8", "markdown", 600) |
| kernel/util/webfetch_test.go:133 | ip-literal-url | text, err := WebFetch("https://203.0.113.10/", "markdown") |
Committed binaries (3)
app/elevator/elevator-amd64.exe: PE (Windows executable), 4 MBapp/elevator/elevator-arm64.exe: PE (Windows executable), 4 MBapp/nsis-resources/x86-unicode/nsProcess.dll: PE (Windows executable), 5 KB
Worst known vulnerabilities (24 of 113)
| Advisory | Severity | Package | Summary |
|---|---|---|---|
| GHSA-23hp-3jrh-7fpw | critical | tar@7.5.16 | node-tar: Decompression/parse DoS via unlimited input |
| GHSA-27p8-2357-5qqv | high | @xmldom/xmldom@0.8.13 | xmldom: DocType `name` Injection Bypasses requireWellFormed |
| GHSA-4w3w-2rp5-g8jm | high | @xmldom/xmldom@0.8.13 | xmldom: Attribute name injection via setAttribute() bypasses requireWellFormed |
| GHSA-8344-3jmq-59r6 | high | @xmldom/xmldom@0.8.13 | xmldom: Quadratic-time attribute deduplication |
| GHSA-93r5-fhx6-vmg9 | high | @xmldom/xmldom@0.8.13 | xmldom: Quadratic-time parsing via the malformed-input recovery path - `parseElementStartPart` re-scan and `normalize()`… |
| GHSA-965w-775f-mr7g | high | @xmldom/xmldom@0.8.13 | xmldom: Quadratic-memory consumption |
| GHSA-c7q8-3ch8-vqpv | high | @xmldom/xmldom@0.8.13 | xmldom: Processing Instruction Target Injection Bypasses requireWellFormed |
| GHSA-w2rr-34g9-rvrj | high | @xmldom/xmldom@0.8.13 | xmldom: Element name injection via createElement() bypasses requireWellFormed |
| GHSA-x4fp-j954-r2f4 | high | @xmldom/xmldom@0.8.13 | xmldom: End-tag Whitespace-Trim Regex ReDoS - quadratic backtracking in the 0.8.x end-tag parser |
| GHSA-27p8-2357-5qqv | high | @xmldom/xmldom@0.9.10 | xmldom: DocType `name` Injection Bypasses requireWellFormed |
| GHSA-3px3-54cx-rmw9 | high | @xmldom/xmldom@0.9.10 | xmldom: Creation-time XML Name/QName validation is bypassable via an embedded line terminator, allowing injection on the… |
| GHSA-4w3w-2rp5-g8jm | high | @xmldom/xmldom@0.9.10 | xmldom: Attribute name injection via setAttribute() bypasses requireWellFormed |
| GHSA-6mj3-qw4j-hgrw | high | @xmldom/xmldom@0.9.10 | xmldom: HTML raw-text closing-tag case mismatch causes output amplification |
| GHSA-8344-3jmq-59r6 | high | @xmldom/xmldom@0.9.10 | xmldom: Quadratic-time attribute deduplication |
| GHSA-93r5-fhx6-vmg9 | high | @xmldom/xmldom@0.9.10 | xmldom: Quadratic-time parsing via the malformed-input recovery path - `parseElementStartPart` re-scan and `normalize()`… |
| GHSA-965w-775f-mr7g | high | @xmldom/xmldom@0.9.10 | xmldom: Quadratic-memory consumption |
| GHSA-c7q8-3ch8-vqpv | high | @xmldom/xmldom@0.9.10 | xmldom: Processing Instruction Target Injection Bypasses requireWellFormed |
| GHSA-g53g-w8rj-fmg7 | high | @xmldom/xmldom@0.9.10 | xmldom PI grammar regex ReDoS: quadratic backtracking on unterminated processing instructions |
| GHSA-vr34-hp96-76pp | high | @xmldom/xmldom@0.9.10 | xmldom: requireWellFormed DocType publicId/systemId validation is bypassable via an embedded line terminator |
| GHSA-w2rr-34g9-rvrj | high | @xmldom/xmldom@0.9.10 | xmldom: Element name injection via createElement() bypasses requireWellFormed |
| GHSA-3jxr-9vmj-r5cp | high | brace-expansion@1.1.14 | brace-expansion: DoS via exponential-time expansion of consecutive non-expanding {} groups |
| GHSA-6j4f-fj2g-mc7p | high | brace-expansion@1.1.14 | brace-expansion: DoS via uncontrolled recursion in parseCommaParts causing stack exhaustion |
| GHSA-mh99-v99m-4gvg | high | brace-expansion@1.1.14 | brace-expansion: DoS via unbounded expansion length causing an out-of-memory process crash |
| GHSA-qhr7-859c-m2p7 | high | brace-expansion@1.1.14 | brace-expansion: DoS via uncontrolled recursion on nested brace groups causing stack exhaustion |
Workflows worth a look
- .github/workflows/target-branch.yml: pull_request_target
By the numbers
| Stars | 46.9K |
|---|---|
| Forks | 3,090 |
| Contributors | 130 |
| Commits | 30.1K |
| Open issues | 32 |
| Open pull requests | 0 |
| Releases | 1,036 |
| Latest release | v3.8.8 |
| Licence | AGPL-3.0 |
| Main language | TypeScript |
| Project age | 6 years |
| Last push | Oct 26, 2026 |
| Tracked files | 6,160 |
| Lines of code | 1.2M |
| Checkout size | 334 MB |
Lines by language: Go 473.8K, TypeScript 392.7K, JSON 103.7K, Markdown 77.2K, JavaScript 74.9K, SCSS 26.4K.
Questions
Is SiYuan free?
Mostly. SiYuan is open source under AGPL-3.0 and most features are free, even for commercial use. Some features, such as using third-party cloud storage for sync, are paid membership privileges; the pricing page on b3log.org lists them.
How does SiYuan store my data?
In a workspace folder on your device: notebooks as folders of .sy files in JSON, plus folders for assets, templates, widgets and plugins. Do not sync that folder with a third-party sync drive, which the project says can corrupt it.
SiYuan or Logseq?
Both are open source, local-first and built on linked blocks. Logseq is outliner-first, centred on a daily journal. SiYuan edits documents in a WYSIWYG Markdown editor and adds SQL queries, database views, flashcards and an official Docker image for self-hosting. Choose by whether you think in bullets or in documents.
This post is part of GitHub Tools, where every repository is cloned and scanned before it is written up. The scan is a snapshot of one commit on one day; the repository has moved on since, so check it before you install.
