
Building a Team Assistant by Running NemoHermes Persistently on DGX Spark and Making It Callable from Slack
This page has been translated by machine translation. View original
Introduction
Hello, I'm Shimada from the Manufacturing Business Technology Department at Classmethod.
I built a resident AI agent that can be called from Slack on the DGX Spark our team uses.
Ultimately, an LLM router automatically dispatches each request to the most suitable model, and I've added Backlog integration and a team RAG on top.
This series consists of 4 articles in total.
- Building a Slack-resident agent with NemoClaw/NemoHermes (this article)
- Automatically routing requests with NeMo Switchyard and a post-trained judge model
- Letting the agent operate Backlog with skills
- Connecting a team RAG and web search to the agent
The final overall architecture is as follows.
This article covers the part between Slack and NemoHermes, that is, keeping the agent resident on the DGX Spark and making it safely callable from Slack.
Note that at the time of writing, NemoClaw is at v0.0.109.
Behavior and settings may change in future updates.
Please check the official documentation for the latest information.
NemoClaw and NemoHermes
NemoClaw is an agent execution platform published by NVIDIA.
It runs the agent itself (selectable from OpenClaw, Hermes, and Deep Agents) in an isolated execution environment called a sandbox.
The sandbox is composed of a Docker container, Landlock, and L7 egress policies, and you can declaratively restrict, via an allowlist, which hosts, which paths, and which methods the agent can access.
NemoHermes is the name for the configuration in which Hermes Agent (by Nous Research) runs on NemoClaw.
Messaging channels such as Slack, Discord, and Telegram are built in, so you don't need to build your own Slack gateway.
When keeping an agent that touches internal data resident, this sandbox design is reassuring.
In the Backlog integration covered in Part 3, I fixed the granularity with network policies so that creating and updating issues is allowed but deleting is not.
Prerequisites
- DGX Spark (GB10, 128GB unified memory, Ubuntu 24.04 ARM)
- Docker (preinstalled. Add your user to the docker group beforehand)
- Node.js 22 (the preinstalled one was v18, so I installed it in user space with nvm)
sudo usermod -aG docker $USER # 要再ログイン
curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
nvm install 22
Installing NemoClaw
I follow the official headless server deployment instructions.
Pin the install source with a commit SHA.
The official docs recommend not using mutable tags such as latest.
For the model, you can specify an OpenAI-compatible endpoint as a custom provider.
For the first connectivity check, I connect directly to a model running on the preinstalled Ollama, and the router (Switchyard) will be inserted in Part 2.
export NEMOCLAW_INSTALL_REF="d027b770d7130685d020964c4811211baf3d5f31" # v0.0.109
export NEMOCLAW_AGENT=hermes
export NEMOCLAW_SANDBOX_NAME=team-assistant
export NEMOCLAW_POLICY_TIER=balanced
export NEMOCLAW_PROVIDER=custom
export NEMOCLAW_ENDPOINT_URL="http://localhost:11434/v1" # Ollama
export NEMOCLAW_MODEL="qwen3.6:35b"
export NEMOCLAW_COMPATIBLE_AUTH_MODE=none
curl -fsSL "https://raw.githubusercontent.com/NVIDIA/NemoClaw/${NEMOCLAW_INSTALL_REF}/install.sh" | \
NEMOCLAW_INSTALL_REF="$NEMOCLAW_INSTALL_REF" \
NEMOCLAW_NON_INTERACTIVE=1 \
NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 \
bash
The sandbox started in about 6 minutes.
An OpenAI-compatible API (127.0.0.1:8642) is also exposed, so you can check connectivity from the CLI.
TOKEN=$(nemohermes team-assistant gateway-token --quiet)
curl -s -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
http://127.0.0.1:8642/v1/chat/completions \
-d '{"model":"hermes","messages":[{"role":"user","content":"一言で自己紹介して"}]}'
{
"id": "chatcmpl-69ed2d…",
"object": "chat.completion",
"created": 1787206201,
"model": "hermes",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "私はNVIDIA OpenShellサンドボックス内で動作するHermes Agent(Nous Research製)です。コード実行、検索、分析、クリエイティブ作業などをツール経由で支援します。"
},
"finish_reason": "stop"
}
],
"usage": { "prompt_tokens": 16548, "completion_tokens": 86, "total_tokens": 16634 }
}
It replied in Japanese.
It also recognizes that it is inside a sandbox.
Let's look at usage. Just by sending "Introduce yourself in one sentence," prompt_tokens is 16,548.
Hermes packs tool definitions and behavioral instructions into the system prompt, so this much is consumed before the conversation even begins.
When estimating context, you need to subtract this constant amount.
Running nemohermes team-assistant status lets you see the actual policies applied to the sandbox.
Inference is restricted at L7: only specific paths such as POST /v1/chat/completions are allowed via the virtual host inference.local, and the curl binary is limited to the pypi and brew endpoints, and so on.
Slack Integration
From here, I'll write out the steps for accessing the agent from Slack.
Generating the app manifest
You don't need to go around researching the OAuth scopes required for the Slack app.
The Hermes CLI inside the sandbox generates the Slack app manifest for you.
C=$(docker ps --format '{{.Names}}' | grep team-assistant)
docker exec "$C" hermes slack manifest > slack-manifest.json
You get a manifest with Socket Mode enabled, 16 bot scopes, and event subscriptions all in place, so paste it into "Create New App" → "From a manifest" at api.slack.com/apps.
One thing to note.
The generated manifest includes 50 slash commands (/new, /stop, /model, etc.).
Slash commands share a namespace across the entire workspace. To prevent the internal workspace from being cluttered with a large number of slash command suggestions, I registered only a single /hermes.
This narrowing has a side effect.
Hermes shows messages in its guidance and banners like "use /new to end the session," but since I haven't registered it, it doesn't work.
If you pass it as a subcommand of /hermes, like /hermes new, it is internally converted to /new and executed.
Registering tokens and Socket Mode
After installing the app and obtaining the Bot Token (xoxb-) and App-Level Token (xapp-, connections:write), register them in the sandbox.
nemohermes team-assistant channels add slack
# → トークン 2 つを貼り付け、メンバーとチャンネルの allowlist を入力
The tokens are validated with auth.test and apps.connections.open before being saved, then a sandbox rebuild runs and the egress for Slack (only 5 hosts, including slack.com and wss-primary.slack.com) is opened.
Because it uses Socket Mode, no inbound port opening is needed.
It works as-is even on a DGX inside the corporate network.
Pitfall: credential validation during rebuild
Here the rebuild stopped at preflight.
Rebuild preflight failed: cannot safely reuse the gateway credential for 'compatible-endpoint'.
Export COMPATIBLE_API_KEY to use normal credential validation and upsert.
The cause was that I chose NEMOCLAW_COMPATIBLE_AUTH_MODE=none (unauthenticated Ollama) in the initial install.
Since there is no credential that can be re-validated at rebuild time, it is designed to err on the safe side and stop.
The fix is simply, as the message instructs, to export a dummy API key and rebuild (Ollama ignores the Authorization header, so there's no actual harm).
COMPATIBLE_API_KEY=ollama-local-noauth nemohermes team-assistant rebuild --yes
If you onboard with an unauthenticated local endpoint, this environment variable will be required for every subsequent rebuild.
It's easier to register a dummy key as the credential from the start.
Access Control
I nailed down two settings for team use.
Member allowlist
If you leave SLACK_ALLOWED_USERS empty, it doesn't open to everyone; it switches to a mode requiring manual pairing requests.
If you want to open it to all members of the team channel, list the member IDs separated by commas.
Even if you re-run channels add slack to update the allowlist, the saved value is reused as "already set" and no prompt appears.
Reading the source, I found that config values are implemented so that environment variables take priority over saved values, so you can update by re-running with environment variables attached.
SLACK_ALLOWED_USERS="U0XXXXXXXXX,U0YYYYYYYYY,..." \
SLACK_ALLOWED_CHANNELS="C0ZZZZZZZZZ" \
nemohermes team-assistant channels add slack # トークンは再貼り付けが必要
Prohibiting DMs
Since this is the team's shared assistant, I limit conversations to channels and block DMs.
This is not a NemoHermes-side setting; instead, remove the im:* and mpim:* scopes and the message.im and message.mpim event subscriptions from the Slack app manifest and reinstall.
The events themselves will no longer reach you on the Slack side.
Additionally, disabling the Messages tab in App Home also blocks the DM input field.
The problem of speaking up on its own in threads
After running it for a while, another issue came up.
In a thread where the bot was @mentioned once, it responds to subsequent messages even without a mention.
It cuts in while people are chatting among themselves, and the team asked for it to be stopped.
This is by design.
Reading the Slack adapter implementation, it remembers the thread_ts of mentioned threads and is built to respond to later messages in that thread even without a mention.
It accumulates them in a set called _mentioned_threads, and a comment also says "to thread replies even without an explicit @mention".
There are multiple response conditions, and besides mentioned threads there are automatic triggers such as "follow-ups to the bot's own messages" and "channels where a session is alive."
A switch to turn all of these off together was available.
| Key | Default | Effect |
|---|---|---|
strict_mention |
false | When true, an @mention is required on every message. Disables all the automatic triggers above |
require_mention |
true | Requires a mention for channel messages |
free_response_channels |
(none) | Makes mentions unnecessary in the specified channels |
Setting platforms.slack.extra.strict_mention to true stops it from remembering threads.
However, there is a pitfall in how this setting is applied, and setting it from inside the sandbox causes a crash.
What happens when you directly rewrite the configuration file inside the sandbox will be covered in a later article.
There is one more thing to note.
This value is lost on sandbox rebuild.
This is because platforms.slack is an area regenerated from the NemoClaw manifest and is not included in the rebuild's state restoration.
channels add slack also involves a rebuild internally, so the same thing happens.
You need to include "reconfigure after a rebuild" in your operating procedures.
Verification
When you mention @Hermes in a channel, the local model on the DGX returns a response.

At this point, we have reached the following state.
- All members who have joined the channel can call it from Slack
- Interaction via DM is not possible
- It responds only when called and doesn't linger in threads
- The agent runs bound by L7 policies inside the sandbox
- All inference is done by local models on the DGX
Conclusion
Using NemoClaw, I was able to stand up a resident agent bound by network policies in less than a day, without building my own Slack gateway.
Three things tripped me up: the rebuild stopping because I onboarded with an unauthenticated endpoint, the convention that environment variables take priority when updating settings, and the behavior of remembering threads and speaking without a mention.
All of them were behaviors with reasons that become clear once you read the implementation, and trying to fix them by guesswork would have been a detour.
However, at this point there is only one model.
Next time, I'll put NeMo Switchyard in between and build automatic routing based on request content. It's the story of how a setup using a plain model as the judge broke down in real operation, and how I replaced it with a judge model post-trained specifically for judging.