Security
Security model.
Hadano stores data.
Hadano retrieves data.
Hadano does not execute stored content.
Those three sentences are the model. Everything on this page says what they mean in the implementation, which of them are held by a machine check rather than by intent, and where the boundary is still thin. A control that is not checked is written here as unchecked, because that is the part you have to decide about.
01 / Reach
What Hadano can access.
Everything the process touches on disk is derived from one string: the value of
HADANO_DB, read once when the server starts. No tool argument names a
path, so nothing a caller sends can widen this list.
| what | access | where the name comes from |
|---|---|---|
| The database file | read and write | HADANO_DB, read at startup |
Its undo journal, | created and removed by the storage engine while a write is in flight | derived from the database file name |
The audit directory, | append; compress and delete files past the retention window | derived from the database file name |
| Standard input and standard output | read and write | the MCP transport itself |
| Standard error | write | startup diagnostics; no document content |
| The Windows permission tools | run with an argument list, never through a shell | only against paths derived from the database file name |
The last row is the only place this software starts another program. It runs the operating system's own access-control utility to tighten a file it just created, with the shell disabled, and it never passes stored content to it.
A path on a network share or inside a cloud-synced folder is refused when the file is opened, with the reason printed, rather than accepted. That refusal is there because file locking is not reliable in those places, but it also bounds where the content can end up.
02 / Boundary
What Hadano cannot access.
| what | what holds it |
|---|---|
| Any path a caller names | None of the seven tools takes a path, file, directory, or URL argument. A test walks the published schema of every tool and fails on a property whose name looks like one |
| The network | No listening socket, no network transport, and no network module in the product code — see the next section for how each part of that is checked |
| A second database | One path per process, fixed at startup. A second process opening the same file is refused at startup with a named reason and a non-zero exit, not a generic error |
| Credentials | No storage column and no tool argument named for one. A test rejects the names credential, password, secret, api_key, and token in both places |
| Other programs | No shell is invoked anywhere, and stored content is never passed to a process |
| Your other files | No path is ever derived from document content, from a query, or from anything else that arrives over the transport |
Two of these rows are enforced by tests that read the tool schema the server actually publishes, rather than the source that is supposed to produce it. A schema that drifts from the intent fails the same way a wrong implementation does.
03 / Network
It does not use the network.
The server speaks the Model Context Protocol over standard input and output. There is no port, no address, and no key. That claim is worth as much as the way it is checked, so here is the way it is checked — three tests at three different distances from the code, and a fourth check on what may be imported at all.
| check | what it does | what it would catch |
|---|---|---|
| Static | Scans the product source for the HTTP and server-sent-event transports and for socket binding, by name, and requires the stdio entry point to be present | Someone switching on another transport |
| In process | Replaces the socket bind call, builds the server, and asserts it was never reached | A bind that happens through a path the name scan does not see |
| Out of process | Starts the server as a real subprocess over stdio, then asks the operating system which sockets that process ID is listening on. The list must be empty | Anything at all opening a port at runtime, including inside a dependency |
| Dependencies | Walks every import in the product source. Only the standard library, the MCP library, the schema library, and the two optional search libraries are allowed | A new third-party package arriving without review |
The one component that would need it
Vector search would download an embedding model on first use. It is not
connected in this version. Starting the server with HADANO_EMBEDDINGS
set to anything other than off is refused with a reason and a
non-zero exit, rather than accepted and quietly ignored — so the network is
not something a configuration mistake can switch on.
What these checks do not assert
All three runtime checks are about listening. Nothing intercepts an outbound connection and asserts it never happens. The reason to believe nothing dials out is that the product source imports no network module at all, which the dependency check covers for third-party packages and review covers for the standard library. If you need that property enforced rather than reviewed, the honest answer today is to run the process without network access and rely on your own sandbox.
04 / Execution
Stored content is never executed.
A document goes in as text and comes out as text. On the way it is cut into chunks at fixed offsets and indexed as character trigrams. Nothing on the store path or the search path parses it as a program, a template, or a query language. The storage layer has no query language to escape from: search compares, it does not interpret.
| property | how it is confirmed |
|---|---|
| Text shaped like an injection is inert | A payload built to end a statement and drop a table is stored, used as a title, and used as a query. Afterwards every document is read back and must be byte-identical, and the unrelated document must still be there |
| Wildcards are literal | % and _ must match themselves. A query for a_b must return the document containing a_b and not the one containing axb |
| Query metacharacters do not crash | Quotes, parentheses, AND, OR, NOT, NEAR(, *, :, ^ are each sent as part of a query. Each must either work or fail with one of the six documented error codes — the taxonomy is closed at six, and a code outside it fails the test as loudly as an unhandled exception would |
| No query is built by string concatenation | A syntax-tree scan of the product source rejects statements assembled from pieces. The detector is itself tested against samples it must catch and samples it must ignore, so it cannot be quietly loosened to silence a false alarm |
| No shell | The only external program is the operating system's access-control utility on Windows, invoked with an argument list and the shell disabled. On POSIX the equivalent work is a direct system call |
Text that arrives is data, including text that sounds like an instruction
A knowledge base an agent reads from is a place where an instruction can be planted for a later reader. Two things are held by tests: tool descriptions are scanned for imperative phrasing, so the server cannot ship a description that tells a model what it must do; and the descriptions of both search tools state that returned text is data. Query text is never treated as an instruction, and results carry no privileged meaning.
This bounds what this software does with the text. It cannot bound what the model on the other end of the transport does with it. A document you store is a document the agent will read, and that judgement stays with you.
What is not machine-checked
There is no test asserting the absence of dynamic evaluation — no scan
for eval, exec, or deserialising stored bytes into
objects. The product code contains none of those, and the dependency check bounds
what could bring them in, but the property rests on review rather than on a gate.
05 / Persistence
What is on disk, and what deletion means.
hadano.db ..................... the database. everything you stored
hadano.db.jrn ................. only while a write is in flight;
holds the bytes about to be overwritten
hadano.db-log/ ................ audit, one file per UTC day
YYYYMMDD.jsonl today. append only
YYYYMMDD.jsonl.gz folded at 7 days, removed at 30
hadano.db.reset-staging/ ...... seconds, during a reset only
hadano-*.db ................... copies you made. plain files.
reset lists them, never deletes them
File permissions
The database, every sidecar of it that exists, the audit directory, every file inside it, and the temporary file used while compressing a day are all tightened to the account running the process. The temporary file is created and tightened before anything is written into it, so there is no window in which a day's log sits readable under a wider set.
| platform | what is set | how it is confirmed |
|---|---|---|
| POSIX | Files 0600, directories 0700 — directories need the execute bit, so they are not given the file mode | The mode is read back immediately after the change; a mismatch raises rather than returning quietly. Tests read the mode of the directory and of every file in it |
| Windows | Inheritance removed, then a single grant to the current account. Directories are granted with object and container inheritance, so files created inside cannot pick up a wider set | The access list is read back immediately after the change and an inherited entry raises rather than returning quietly. Tests read the same list and require that no inherited entry survives and that no account other than the granted one holds a write entry |
Confirmed on Windows 11 x64, Python 3.14, built with MSVC — the only platform this storage engine has been built and tested on. The POSIX branch is implemented, but it has never been run on a POSIX machine: its tests skip on the one platform where the suite actually runs, so nothing in the POSIX row is claimed to have passed anywhere. Treat that row as written, not as verified.
Deletion
| what you run | what it removes | what it leaves |
|---|---|---|
delete_document | The document and everything derived from it — chunks, index entries, edges — in one transaction, so nothing derived outlives the document | The freed space returns to the file's free list and is reused. The old bytes are not overwritten |
| Audit retention | Log files older than the retention window, 30 days by default and overridable with HADANO_LOG_DAYS. Files older than 7 days are compressed first; that 7 is fixed, not configurable | Nothing turns auditing off. A malformed, zero, or negative retention value falls back to the 30-day default rather than disabling it |
python -m hadano.reset --db | The database file, its journal if one is present, and the whole audit directory — by file operations only. It never opens the engine | Copies, exports, sync destinations, shadow copies, hard-link aliases, and the bytes on the storage device |
Reset is the one destructive command, and it is built to be hard to trigger by
accident. By default it only lists. Deleting requires --apply plus the
database's file name typed again after --confirm. It refuses
symbolic links and junctions, a zero-byte file, a file that is not a Hadano
database, a name with a trailing space or dot, any path on a network share or in a
synced folder, and a staging folder left behind by a previous run. It moves
everything into a staging folder in the same directory first and deletes in one go,
so a failure part-way puts things back instead of leaving half. It exits 0 only when
every target is gone; a partial deletion exits 4 and names the files that remain.
Afterwards it prints what it did not touch, because saying “everything is gone” would be false. It also cannot stop your MCP client: if the client is still running when you reset, the next call creates a new empty database and a new log directory.
The audit record
Every call writes one line, whether it succeeded or not. What the line carries
depends on HADANO_REQUEST_LOG: redacted, the default,
records the type and length of each argument; full keeps the first
1,000 code points of each string argument and marks how many were cut;
off empties the argument object. In all three the request ID, the tool
name, a hash of the arguments, the row count, the duration, and the outcome are
recorded. A hash is not the content, but it is derived from it — identical
arguments produce identical hashes, so the log shows when the same call was made
twice.
The log is append-only because the only way in is an append; no code path issues an update or a row deletion against it. Timestamps and file names are UTC, so a day is a day everywhere. If tightening the permissions on a log file fails, the line is still written and the failure is recorded — a missing audit line is treated as the worse outcome. Lines that cannot be parsed on read-back are counted and reported rather than silently skipped.
One line per call holds for a single client holding the database. Concurrent writers sharing one audit file can lose lines. That is a known limitation of this build, not a property this page claims.
Where this is still thin
Written plainly, because these are the parts that decide whether the model above is enough for you.
- Deletion removes reachability, not bytes. A deleted record's space goes back to the free list and is reused later, and the full-text index is keyed on fragments of the text itself, so the content of a deleted document can be reconstructed from what remains in the file. If the content has to be unrecoverable, encrypt the disk — there is no encryption at rest in this software, by design, and it is delegated to the operating system
- The permission pass runs at startup. The undo journal exists only during a write, so a journal created after startup carries the permissions of the directory it is made in rather than an explicit tightening
- The directory holding the database is yours. Nothing changes its permissions. The audit directory is tightened when this software creates it; one that already exists is left as it is
- The startup integrity check is not a checksum. It reports whether the previous run ended abnormally and whether any indexed document is still unflushed — both conditions that make results incomplete. It does not verify the file's contents, and the index has no checksums yet
- Copies are outside all of this. A backup made by copying the file, a sync destination, or a shadow copy carries the content and none of the permission work. Snapshot backup and JSONL export do not yet work against the current storage engine and are being rebuilt for it; until they land, a backup is a copy of the file
06 / Threat model
Who this defends against.
The file and the process belong to the person running them. The boundary this design defends is between the data and the code: a document, a query, or a database file — including one produced by an untrusted third party and handed to the user — must not make the engine do something the caller did not ask for, or reach outside the paths the caller supplied.
| defended | not defended |
|---|---|
| Content from an untrusted source being stored, indexed, and read back | The person running the process. They already control the file and the code |
| Text that looks like a query, an instruction, or a path | Anyone who can read the file. There is no encryption at rest; that is the disk's job |
| Arguments that try to escape the published tool schema | Anyone who can run code as you. They can do everything this software can do |
| A crafted database file reaching the storage engine | Separation between tenants. One process, one file, one user — there is nothing to separate |
There are operating limits — a 2,000 ms wall-clock budget per
tool call, and at most three search seeds per expansion, both fixed in this build;
the edges a knowledge expansion returns are bounded by the caller's own
top_k, and a result that was cut says so rather than coming back
silently short. These exist so that a call ends, not to keep the caller out. A local
user making their own queries slow is not a vulnerability.
In scope for a report
- Reading or writing files outside the paths the caller supplied
- A crafted database file causing memory corruption in the storage engine
- Query construction that escapes its intended scope
- The server accepting input that escapes the argument schema
- Backup, dump, or restore producing or accepting data that violates the documented format in a way that is exploitable
- A documented control that does not do what it says
Out of scope
- Denial of service through resource exhaustion by the local user
- The hosted service, its authentication, billing, and console. Those are a separate product and are not this software
- Findings from automated scanners with no demonstrated impact
- Dependency vulnerabilities already public and fixed upstream — report those upstream, and tell us if we are pinned to a vulnerable version
07 / Reporting
Reporting a vulnerability.
Please do not open a public issue for a security problem. Email
info@tech-japan.jp with the subject SECURITY.
- What the problem is, and what an attacker gains from it
- The smallest input or sequence of calls that reproduces it
- The version or commit you tested, and your platform
We acknowledge within five business days and tell you whether we consider it in scope, what we intend to do, and roughly when. There is no paid bounty. We credit you in the release notes for the fix unless you ask us not to.
There is no stable release yet, so there is no supported version range to state: only the current development line receives fixes. When releases begin, that will be written here instead.