NAI OS

The tools roster

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.

Three layers, in order

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
The two providers on the left, the permission file in the middle, the four agent files that actually hold an 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.

Where the tools come from

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.”

ProviderTool prefixHeld byHow 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 contactscontacts 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.”

Which Google service is which

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.

ServiceGranted in an agent's tools:To whomExposed 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.”

Other tools

CategoryToolsUsed 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.

The second layer: .claude/settings.json

A 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.

ListEntriesOf which
permissions.allow16 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.deny56 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.

Keeping Qonto off between quarters

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.

nai-analysis — a route that isn't a subagent tool

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 — the same shape, one layer up

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 Bashtools: 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.

GitHub as an input route, not only an output

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.”

StepWhat it producesIs 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.

Next/nai-issue-to-agent