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.

whataccesswhere the name comes from
The database fileread and writeHADANO_DB, read at startup
Its undo journal, .jrncreated and removed by the storage engine while a write is in flightderived from the database file name
The audit directory, -log/append; compress and delete files past the retention windowderived from the database file name
Standard input and standard outputread and writethe MCP transport itself
Standard errorwritestartup diagnostics; no document content
The Windows permission toolsrun with an argument list, never through a shellonly 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.

whatwhat holds it
Any path a caller namesNone 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 networkNo 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 databaseOne 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
CredentialsNo 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 programsNo shell is invoked anywhere, and stored content is never passed to a process
Your other filesNo 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.

checkwhat it doeswhat it would catch
StaticScans 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 presentSomeone switching on another transport
In processReplaces the socket bind call, builds the server, and asserts it was never reachedA bind that happens through a path the name scan does not see
Out of processStarts 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 emptyAnything at all opening a port at runtime, including inside a dependency
DependenciesWalks every import in the product source. Only the standard library, the MCP library, the schema library, and the two optional search libraries are allowedA 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.

propertyhow it is confirmed
Text shaped like an injection is inertA 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 crashQuotes, 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 concatenationA 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 shellThe 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.

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.

platformwhat is sethow it is confirmed
POSIXFiles 0600, directories 0700 — directories need the execute bit, so they are not given the file modeThe 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
WindowsInheritance 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 setThe 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 runwhat it removeswhat it leaves
delete_documentThe document and everything derived from it — chunks, index entries, edges — in one transaction, so nothing derived outlives the documentThe freed space returns to the file's free list and is reused. The old bytes are not overwritten
Audit retentionLog 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 configurableNothing 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 --apply --confirm The database file, its journal if one is present, and the whole audit directory — by file operations only. It never opens the engineCopies, 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.

defendednot defended
Content from an untrusted source being stored, indexed, and read backThe person running the process. They already control the file and the code
Text that looks like a query, an instruction, or a pathAnyone 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 schemaAnyone who can run code as you. They can do everything this software can do
A crafted database file reaching the storage engineSeparation 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.