NAI OS
Every tool a subagent can reach, grouped the way the big picture groups them — web, mailbox, bank, files — where each one comes from, the three layers that narrow it on the way down, and the routes out that aren't subagent tools at all.
The big picture draws Tools as two boxes: a general
Other tools box every subagent can reach, and nai-analysis, reachable
only through the /nai-issue-to-agent bridge. This page is what's actually inside both
boxes — pulled straight from each agent's own tools: frontmatter, not a
paraphrase — plus the layer above it, where those tools are provisioned, and the layer between,
where the harness itself refuses some of them.
A tool name in an agent's tools: line is the last of three narrowings, not
the only one. Above it sits the server or connector, which decides what exists at all; between them
sits .claude/settings.json, which the harness applies before any agent is dispatched.
paper-ingester is the only agent file that names the middle layer in its own body, and
it states the design intent plainly: “This isn't prose-only: .claude/settings.json
denies any mv/cp/curl Bash command whose arguments reference
library-wiki...wiki at the permission layer, ahead of execution — a second, structural
backstop under this instruction, not a replacement for following it.”
%%{init: {'theme':'neutral', 'flowchart':{'htmlLabels':false,'nodeSpacing':26,'rankSpacing':110}}}%%
flowchart LR
subgraph L1["1 - server scope"]
gw["google-workspace (local uvx server)"]
qo["Qonto (claude.ai connector)"]
end
subgraph L2["2 - .claude/settings.json"]
gwp["no google-workspace entry"]
qop["9 allow / 53 deny"]
end
subgraph L3["3 - agent tools: frontmatter"]
a1["calendar-briefing (2 Calendar tools)"]
a2["email-triage (3 Gmail tools)"]
a3["qonto-collector (9 Qonto tools)"]
a4["loan-interest-vga (list_transactions)"]
end
gw --> gwp
qo --> qop
gwp --> a1
gwp --> a2
qop --> a3
qop --> a4
subgraph GH["beside the layers - no server, no mcp__ prefix, no tools: line"]
orch["/nai-paper-ingest orchestrator (not a subagent)"]
cli["Bash, shelling out to the gh CLI"]
iss["GitHub issue on this repo: 1 view, 1 comment"]
hook["Stop hook (.claude/settings.local.json)"]
rem["git push to origin main"]
end
orch --> cli
cli --> iss
hook --> rem
classDef provider fill:#e8eef7,stroke:#2b5f9e,stroke-width:2px
classDef perm fill:#f3e9f3,stroke:#7a3f7a,stroke-width:2px
classDef subagent fill:#fdf3e0,stroke:#8a5a00,stroke-width:1.5px
classDef github fill:#e3f0e6,stroke:#2f6b46,stroke-width:2px
class gw,qo provider
class gwp,qop perm
class a1,a2,a3,a4 subagent
class orch,cli,iss,hook,rem github
mcp__ tool on the right. The middle column is not
symmetric: .claude/settings.json carries nine mcp__claude_ai_Qonto__
allow entries and fifty-three deny entries, and no mcp__google-workspace__ entry of
either kind. Of the 25 files in .claude/agents/, only these four hold an
mcp__-prefixed grant; the other 21 are Files/Web only. GitHub is on the same
canvas but not in the stack, and the placement is the finding: it enters at the
orchestrator, which runs Bash to the gh CLI
(“gh issue view <n> --json number,title,body,url,state”), so it
passes through none of the three narrowings. Layer 1 never registers it — there is no MCP server
for GitHub and no mcp__github__ prefix to carry. Layer 3 never names it: the one
subagent downstream of a GitHub read holds tools: Bash, Read, Glob, WebFetch, and its
own boundary spends that Bash elsewhere. Layer 2 has exactly one gh
entry, Bash(gh repo *), and it matches neither call that is actually made. The
second strand in that box is the same shape again: “The Stop hook (Step 2) auto-pushes to
origin main after every turn” — a write to GitHub issued by a hook, not by any
tools: line.Two providers supply everything outside this repo, and they are provisioned in two entirely
different ways — which is why the prefix in a tool name is worth reading. Neither is in git:
“None of these are in the repo — there is no .mcp.json, so every
server is user-scoped and a new machine starts with zero. Nothing that touches Gmail, Calendar, or
Qonto works until this is done.”
| Provider | Tool prefix | Held by | How it is provisioned |
|---|---|---|---|
google-workspace |
mcp__google-workspace__ |
calendar-briefing (list_calendars, get_events);
email-triage (search_gmail_messages,
get_gmail_message_content, draft_gmail_message) |
An MCP server registered locally and run on this machine:
claude mcp add google-workspace -s local … -- uvx workspace-mcp --tools gmail
calendar chat. It carries its own Google OAuth client id and secret — “the client
secret JSON lives at C:\Users\udoni\.config\gws\client_secret.json, in Google Cloud
project ninkit” — and a machine-local token cache. Scoped at the server by the
--tools flag list. |
| Qonto | mcp__claude_ai_Qonto__ — the claude_ai_ segment is the tell |
qonto-collector (9 read tools); loan-interest-vga
(list_transactions only) |
Not a local server at all: “A claude.ai connector — authorize it in your
claude.ai connector settings”. Nothing is installed or configured repo-side, so there is no
--tools-style restriction to apply at registration; the narrowing happens instead in
.claude/settings.json and in each agent's frontmatter. |
The setup doc and the machine doc disagree on one flag, and the machine doc is the one that
reports the live state: “The setup doc's Step 3 command is one flag behind the live
server. As registered today it runs uvx workspace-mcp --tools gmail calendar chat
contacts — contacts included, because get_messages resolves
display names via the People API and fails without it.” The cost of that flag is recorded
deliberately rather than glossed: “contacts grants contacts
and contacts.readonly — i.e. read/write access to Google Contacts —
purely so get_messages can resolve display names.”
The google-workspace registration is where the whole layering idea is stated
outright. The setup doc chose --tools over the server's alternative
--permissions SERVICE:LEVEL flag on purpose, “matching the pattern already used
here: broad OAuth scopes at the server, narrow enforcement at the subagent
tools: whitelist” — and then names the consequence in the same breath:
“Note the consequence — --tools ... chat requests the Chat write scopes
too, so message-sending tools are exposed at the server level. Any agent that must not post to Chat
has to exclude those tools in its own frontmatter, exactly as email-triage excludes
send_gmail_message.”
“google-workspace” is one server name covering four distinct Google
services, and the page-level tool names don't say which is which. The setup doc does, service by
service — its own title is “Google Workspace MCP Setup (Gmail + Calendar + Chat)”, and
the live registration adds a fourth. Split out, the five granted tools land in exactly two of the
four services, and the other two are provisioned but unconsumed.
| Service | Granted in an agent's tools: | To whom | Exposed at the server, granted to nobody |
|---|---|---|---|
| Gmail | search_gmail_messages (“Gmail — search/list”),
get_gmail_message_content (“Gmail — get message content”),
draft_gmail_message (“Gmail — create draft”) |
email-triage, and only it — “You may ONLY read messages and CREATE DRAFTS.
You must NEVER send, reply-send, delete, trash, archive, or otherwise modify the mailbox.” |
send_gmail_message, modify_gmail_message_labels,
batch_modify_gmail_message_labels — verified present on the live server, and
“email-triage grants none of them”. |
| Calendar | list_calendars (“Calendar — list calendars”), get_events
(“Calendar — list events”) |
calendar-briefing, and only it — “you NEVER create, modify, move, or delete
events.” |
Nothing recorded in these sources beyond the two read tools. |
| Chat | None. | Nobody. | All six: list_spaces, get_messages, search_messages,
download_chat_attachment, and the two marked WRITE, send_message and
create_reaction. |
| Contacts (People API) | None. | Nobody. | The whole service. It is in the live --tools gmail calendar chat contacts list for
one reason only: “get_messages resolves sender display names via the People
API”. |
Chat is the sharpest illustration on this page of “broad OAuth scopes at the server, narrow
enforcement at the subagent tools: whitelist”, because the capability is not
merely theoretical — it is finished and proven. The server exposes six Chat tools (“28 tools
total, 6 of them Chat”), the project-level prerequisite is done (“Chat app
configured in the Console (USER)”), and both directions were smoke-tested:
“Smoke test passed: list_spaces returns real spaces; send_message
posted to the Agents space (spaces/AAQAHnZQMS0) 2026-07-14” and
“get_messages verified reading the Agents space 2026-07-14 (output
matched the Chat web UI exactly)”. And still no file under
.claude/agents/ grants a single Chat tool — a grep for
mcp__google-workspace__ across all 25 agent files returns only the three Gmail and two
Calendar names. That is a working capability with no consumer, which is exactly what the doc asks
for: “The two WRITE tools above are the Chat equivalent of send_gmail_message.
Keep them out of the tools: frontmatter of any read-only subagent.” One
prerequisite for using them from a session is still open in the doc's own checklist:
“[ ] Restart Claude Code so the session picks up the Chat tools natively (until then they are
only reachable by driving the server directly)”.
No mail client other than Gmail is in play. Worth recording because it has
been misremembered: email-triage runs on the google-workspace server's
three Gmail tools, named verbatim in its own frontmatter and reconciled against a live probe
(“.claude/agents/calendar-briefing.md and
.claude/agents/email-triage.md's tools: frontmatter already match the
verified names above exactly”), and nothing in the fleet uses Spark — zero
references across the 25 agent files, the 12 command files,
.claude/settings.json, and the setup and machine docs. Since “every server is
user-scoped” and none is in the repo, that absence is precise rather than absolute: no
agent or command in this repo grants, names, or routes through Spark, and the only
documented mail path is Gmail.
Tool totals for that server come from two probes of two different registrations, and they are not
interchangeable: 36 tools with contacts (“uvx workspace-mcp --tools gmail
calendar chat contacts, server google_workspace v3.4.4, 36 tools total”)
and, without it, “28 tools total, 6 of them Chat”. Changing that list is not free:
“Changing the --tools list invalidates the cached token. The
token in token_cache.json only carries the scopes consented to at the time it was
issued.”
| Category | Tools | Used by |
|---|---|---|
| Files | Read, Write, Glob, Grep, Bash |
The baseline for nearly every subagent — reading dropped inputs and prior-round files,
writing the file it's responsible for. Bash is granted to two agents, each for one
narrow, stated purpose: qonto-collector shells out for receipt downloads;
paper-ingester shells out for exactly two things — curl-downloading
a resolved PDF, and mv/cleanup to place it — and holds no Write at
all, since everything it produces lives outside this repo. |
| Web | WebSearch, WebFetch |
calendar-briefing, company-research, option-verifier,
tech-radar (all four); voice-profiler and paper-ingester
(WebFetch only — the former re-fetches one known index page, the latter resolves
one candidate link to its PDF; neither searches). Built in — no server behind them, and nothing in
.claude/settings.json about them. |
| Mailbox & calendar | mcp__google-workspace__list_calendars, get_events,
search_gmail_messages, get_gmail_message_content,
draft_gmail_message |
calendar-briefing (calendar tools, read-only); email-triage (Gmail
tools — read + create-draft only, no send/delete tool is ever granted). |
| Bank | mcp__claude_ai_Qonto__* (9 distinct read endpoints: organization, transactions,
attachments, client/supplier invoices, credit notes, statements) |
qonto-collector (all 9, read-only); loan-interest-vga
(list_transactions only, to match loan transfers by reference). |
Two surfaces are deliberately absent from that table, and their absence is the finding:
GitHub and nai-analysis. Neither is a subagent
tool. Both are reached by the orchestrator shelling out through Bash — one to the
gh CLI, one to a pair of Node scripts — so no tools: line anywhere
mentions either. Both get their own section below.
.claude/settings.jsonA subagent's tool grant is not the only thing standing between this fleet and a mutating banking
endpoint. .claude/settings.json — tracked in git, unlike the servers themselves —
carries a permissions.allow and a permissions.deny list that the harness
applies independently of any agent's frontmatter. It is the defence-in-depth layer: even an agent
mistakenly granted a write endpoint, or a session driving the connector by hand, meets the deny list
first.
| List | Entries | Of which |
|---|---|---|
permissions.allow | 16 | 6 Bash(…) patterns (sort -u -t: -k3, gh repo *,
git log *, git add *, git commit -q -m ' *,
curl -sSL -o "06 Tax Agent/*), 1 Read(//c/Users/udoni/.claude/**), and
9 mcp__claude_ai_Qonto__ tools. |
permissions.deny | 56 | 3 Bash(…) patterns — mv *library-wiki*wiki*,
cp *library-wiki*wiki*, curl *library-wiki*wiki* — and
53 mcp__claude_ai_Qonto__ tools. |
So 62 Qonto tool names are written into this one file, and nine of them are reachable. Those nine
— get_attachment, get_organization, get_statement,
list_client_invoices, list_credit_notes, list_statements,
list_supplier_invoices, list_transaction_attachments,
list_transactions — are the same nine in qonto-collector's
tools: line, tool for tool. The two layers agree here rather than one covering for the
other, and that agreement is the point: machine-setup.md describes the division of
labour as “the guard that's always on”, with the connector toggle as the removable part.
(62 is the count named in this file; no source here states the connector's full
catalogue.)
One nuance the file itself settles. machine-setup.md describes the deny list as
covering “every Qonto tool the pipeline doesn't use (anything that creates, changes, sends, or
approves)”, but the 53 denied names include plainly read-only ones —
mcp__claude_ai_Qonto__get_transaction, get_client,
get_supplier_invoice, get_label, get_qonto_public_pricing,
list_cards, list_clients, list_teams,
list_cash_flow_categories among them. The operative rule is default-deny by
non-use, not deny-by-write: a read tool the tax pipeline has no need for is denied just the
same as a transfer tool.
The asymmetry is worth stating rather than smoothing over: zero
mcp__google-workspace__ entries appear in .claude/settings.json
— neither allow nor deny. As configured today, this second layer governs Qonto and three
Bash patterns, and nothing else. Google Workspace is narrowed at the server (the
--tools flag) and at the agent (tools: frontmatter), with nothing in
between. No source in this repo explains why.
One operational practice is written down, and it applies to the bank connector only:
“treat Qonto as off by default, on for the quarterly close”. In full:
“Disable it when you're not closing a quarter: in Claude Code, /mcp → select the
Qonto connector → disable (or toggle it off in your claude.ai connector settings)”, then
“Re-enable it for the close, verify /mcp reports Connected, run
/nai-tax-theo, then disable it again.” The machine-setup verification checklist
carries the same rhythm: “/mcp reports google-workspace
Connected, and Qonto Connected when enabled — then disable Qonto
again until the next quarterly close”.
Two reasons are given, and they are worth separating. The first is cost, the second is surface:
“Every other session pays the connector's context cost — its tool schemas and server
instructions — for nothing, and a connected banking API is surface you don't need while editing
prompts or drafting a newsletter.” It is affordable because the consumer set is so small:
“Only /nai-tax-theo uses Qonto (the qonto-collector and
loan-interest-vga subagents) — /nai-payroll-theodora never touches
it.” And the toggle is explicitly not the guarantee — the deny list is:
“Independent of this toggle, .claude/settings.json deny-lists every Qonto tool the
pipeline doesn't use … so even a connected session only ever exposes the nine read tools … The
toggle removes the remaining nine plus the server's instruction block; the deny list is the guard
that's always on.”
There is no equivalent practice for google-workspace, and none should be inferred:
the only recurring operation documented for it is re-consent when its scope changes. Nor are the
server definitions themselves anywhere in git — /nai-cohort-census asks whoever
replicates its scan by hand to count “MCP servers in .mcp.json”, and in this
repo that field reads zero, because no such file exists.
Nothing in the tables above ever touches nai-analysis. That access path exists
entirely outside the subagent tools: system, and outside MCP altogether:
/nai-issue-to-agent (the orchestrator itself, not a subagent) shells out via
Bash to two Node scripts in scripts/issue-bridge/ —
fetch-issue.mjs and post-comment.mjs — which hold a Supabase service-role
key and talk to nai-analysis's database directly. Both scripts are deliberately
narrow: “The service-role key stays in the local, gitignored .env — never
committed. fetch-issue.mjs / post-comment.mjs only ever touch the one issue
id passed as an argument (no scan/list queries) and only ever insert one row into
issue_comments — they never edit an issue's content or status, and never update or
delete anything.” That's the whole reason this is drawn as its own box on the big picture
instead of folded into “other tools” — it's a different kind of access, at a different
layer, held by the orchestrator instead of a subagent, and no permission list or MCP server sits
anywhere along it.
GitHub is the second route of exactly that shape, and it belongs on this page for the same
reason: it is real, remote, write-capable access that no tools: line anywhere will ever
show you. Exactly one command uses it. /nai-paper-ingest reaches GitHub by shelling out
through Bash to the gh CLI, twice, both times from the orchestrator:
gh issue view <n> --json number,title,body,url,state to fetch the issue, and
gh issue comment <n> --body "<summary>" to post the result. There is no MCP
server for GitHub in this repo and no agent frontmatter that names it.
The subagent it dispatches never gets near it. paper-ingester does hold
Bash — tools: Bash, Read, Glob, WebFetch — but its own boundary spends
that grant elsewhere: “Bash is granted for exactly two purposes:
curl-downloading a resolved PDF URL to a system temp path, and mv/cleanup
to place the downloaded file into work/<Topic>/ and remove the temp copy
afterward. Never use it for anything else — no arbitrary shell, no touching any file this process
didn't itself just create.” The write into GitHub is held one layer above the agent
tools: system, and it is bounded in prose at exactly the point it happens:
“Never close the issue, never add/remove a label, never edit its title or body — this comment
is the only change you make to the issue.”
Neither gh call is pre-approved. The only entry in
.claude/settings.json's allow list that matches gh at all is
Bash(gh repo *). gh issue view and gh issue comment do not
match it, so nothing in the tracked permission layer waves them through — they prompt. The
repo's one write into GitHub issue data is, on the tracked evidence, the call that has to be
confirmed by hand each time. Two caveats, both honest: no command or agent file in this repo
invokes gh repo, so nothing here explains why that entry exists; and the
per-machine allowlist lives in .claude/settings.local.json, which is gitignored
(“it is full of absolute C:\Users\udoni\... paths”) and therefore
outside what this page can see.
One issue read and one comment posted is a small surface, and it would be easy to file GitHub
under plumbing. What argues against that is the material that travels along it.
11 Blog/blog-ideas.md is the fleet's reading backlog — it opens by saying it is not a
draft (“Not a blog post. This is a backlog file. Do not run
/nai-document blog-ideas against it”) — and it records where the material comes
from and how much of it there is: “Source: Google Chat space Agents
(spaces/AAQAHnZQMS0), read 2026-08-08. Nine messages in the space, all posted by Udo;
seven carry links, two are hello / hi from setting the space up on
2026-07-14 and are not captured below.” Three of those seven are papers, and the file names
GitHub as the way out of the backlog rather than as a filing detail: “Items 3, 6 and 7 are
arXiv papers. Filing each as a GitHub issue on this repo routes them through
/nai-paper-ingest into the research library, which is a better home for the PDF than
this list. Item 7 has already been through it.”
So a GitHub issue on this repo is not only where /nai-paper-ingest reports — it is
the documented trigger, the step a human takes to move an item from a backlog line into the
research library. The chain has been walked end to end exactly once, and the file says which item:
“already ingested. This is the same paper as GitHub issue #18, filed to
work/AI/ by /nai-paper-ingest and closed on 2026-08-08. The PDF is in the
research library; only the blog post is outstanding.” That is item 7, AgentRadio,
arxiv.org/abs/2607.28430 — and the resulting draft is in the repo:
11 Blog/shelf-life-shorter-than-peer-review.md opens
“[AgentRadio](https://arxiv.org/abs/2607.28430) makes an unusually clean argument.”
| Step | What it produces | Is GitHub on this step? |
|---|---|---|
A link posted in the Google Chat space Agents |
A backlog item in 11 Blog/blog-ideas.md — 7 of 9 messages carry links |
No. Read by hand, not by an agent: no file under .claude/agents/ holds a Chat
tool. |
| File the paper item as a GitHub issue on this repo | The issue — #18 for the one item that has been through | Yes, and by hand. Nothing in the fleet creates the issue; the backlog file
states this as the human step (“Filing each as a GitHub issue on this repo routes them
through /nai-paper-ingest”). |
/nai-paper-ingest, step 1 |
gh issue view <n> --json number,title,body,url,state |
Yes — orchestrator Bash, no tools: line involved. |
paper-ingester |
One PDF under H:\Geteilte Ablagen\knowledge\work\<Topic>\ — for item 7,
work/AI/ |
No. tools: Bash, Read, Glob, WebFetch, and that Bash is fenced to
curl and mv. It is handed “candidate links found in a GitHub issue
on this repo”, never the issue itself. |
/nai-paper-ingest, step 4 |
gh issue comment <n> --body "<summary>" |
Yes — the one write, bounded to that comment. |
A hand-written draft in 11 Blog/ |
shelf-life-shorter-than-peer-review.md |
No. “hand-written blog drafts, one markdown file per post … nothing generates them.” |
/nai-document, blog lane |
A page under docs/blog/ |
No. |
The honest shape is that the route is real and proven, not yet the default. Two more backlog
items name /nai-paper-ingest in their own notes — item 3, “Note:
paper link — a candidate for /nai-paper-ingest if you file it as a GitHub issue”,
and item 6, “Note: paper link — candidate for
/nai-paper-ingest” — and both already have drafts in 11 Blog/:
nothing-detects-what-was-dropped.md (“A paper came past titled [Agentic Context
Management](https://arxiv.org/abs/2607.21503)”) and
it-isnt-object-oriented-its-typed.md (“NVIDIA labs published [OO
Agents](https://arxiv.org/abs/2607.20709), proposing that an agent should be a Python
class.”). All three arXiv items in the backlog reached a draft; only one of the three went
through the GitHub issue on the way. The issue step is what puts the PDF in the research library —
it is not what produces the post.
Two limits on the paragraph above, both worth stating. Issue #18 cannot be checked
from this repo. Its number, its closure date, and the filed PDF are attested only by
11 Blog/blog-ideas.md's own prose; the research library it names lives outside this
repo (“The library root is H:\Geteilte Ablagen\knowledge\ — its
work\ subfolder is where you place files”), so nothing tracked here
corroborates it. And GitHub appears in the drafts in a second, unrelated sense:
as cited subject matter. nine-lines-of-cargo-toml.md links
“[anydoc](https://github.com/firecrawl/anydoc), from Firecrawl” and
a-check-the-agent-cant-talk-past.md links
“[LifeOS](https://github.com/danielmiessler/LifeOS)” — public third-party repositories
reached by the web tools any researching subagent already holds, not by gh, and not
on the route described here.
The two orchestrator routes look alike and are not the same system, which is worth stating
because the wording in one command file invites the mix-up. /nai-issue-to-agent's
argument parser says its regex “handles both bare UUIDs and pasted GitHub issue URLs (they
contain the UUID)” — but the bridge it calls goes to Supabase, not GitHub:
“Bridges nai-analysis issues (Supabase)”. CLAUDE.md draws
the line the other way round for the gh route, and just as plainly:
/nai-paper-ingest “reads one GitHub issue on *this* repo (not
nai-analysis)”. Different host, different credential, different issue tracker,
one comment each.
There is a third way this repo talks to GitHub, and it sits outside the tools system altogether:
plain git. The repo is cloned from
https://github.com/udopnink/nai-os.git, and the Stop hook
“auto-commits and pushes to origin main after every turn (retrying once with
git pull --rebase --autostash if the push is rejected because the clone is
behind…)”. The tracked allow list pre-approves git log *, git add *
and git commit -q -m ' * — and carries no git push entry; the push runs
from the hook, which lives in that same gitignored .claude/settings.local.json. So of
the three GitHub paths, the only one that is fully visible in tracked files is the one that writes
the least.