MOTD Dashboard with Claude Code in One Prompt
Ubuntu’s default login banner wastes the first few seconds of every SSH session. I replaced it with a custom MOTD dashboard built by Claude Code that shows system health, Docker services, firewall status, and pending security updates before I type a single command.
Claude Code is Anthropic’s agentic coding tool that runs in your terminal. It can read files, execute commands, and iterate on its own output. I gave it a single prompt, and it scanned the server, discovered what was actually running, wrote a Bash script in /etc/update-motd.d/99-dashboard, and iterated until the output stopped breaking on edge cases.
This post shows the result, the prompt, and the implementation details that mattered.
Table of Contents
- The Default MOTD Problem
- What the MOTD Dashboard Shows
- How Claude Code Builds the MOTD Dashboard
- The MOTD Dashboard Prompt for Claude Code
- What Happened on My Server
- Lessons Learned
- Frequently Asked Questions
The Default MOTD Problem
If you have used Linux servers before, you have seen /etc/motd — a static text file displayed after login. Ubuntu and Debian extend this with /etc/update-motd.d/, a directory of scripts that pam_motd runs dynamically on every interactive SSH or console login. The idea is good. The default scripts are not.
Ubuntu ships with a dozen of these scripts, and most of them exist to upsell you on Ubuntu Pro or tell you things you already know. Here is what you get on a fresh Ubuntu 24.04 server:
Welcome to Ubuntu 24.04.1 LTS (GNU/Linux 6.8.0-101-generic x86_64)
* Strict classic confinement of snap...
* Introducing Expanded Security Maintenance...
5 updates can be applied immediately.
5 of these updates are standard security updates.
1 additional security update can be applied with ESM Apps.
Learn more about enabling ESM Apps service at https://ubuntu.com/esm
No CPU load. No memory usage. No disk space. No Docker status. No firewall info. Nothing that helps you understand the state of the machine you just logged into.
One common fix is to chmod -x the noisy defaults and replace them with something useful. A caveat: package updates can re-enable those scripts. A more durable approach is to configure pam_motd itself in /etc/pam.d/sshd — but for most setups, chmod -x works and is easy to redo if an apt upgrade flips the bits back. Also make sure PrintMotd no is set in /etc/ssh/sshd_config so OpenSSH does not print its own static /etc/motd on top of the dynamic output.
What the MOTD Dashboard Shows
After running Claude Code, my SSH login looks like this:
┌──────────────────────────────────┬───────────────────────────────┐
│ mail.example.com │ 203.0.113.42 │
│ Mon 09 Mar 2026 14:17:04 CET │ up 1 day, 16 hours │
└──────────────────────────────────┴───────────────────────────────┘
⚙ SYSTEM
──────────────────────────────────────────────
CPU AMD EPYC 9645 (8 cores)
Load 0.21 0.38 0.41 (1/5/15 min)
RAM [██████░░░░░░░░░░░░░░] 5020M / 15986M (31%)
Disk / [█░░░░░░░░░░░░░░░░░░░] 14G / 1007G (2%)
Swap none
Net ▼ 2.8 GB rx ▲ 336.8 MB tx (eth0 since boot)
Procs 451
🐳 DOCKER SERVICES
──────────────────────────────────────────────
▸ mailserver
● mail-web-1 Up 22 hours
● mail-db-1 Up 22 hours (healthy)
● mail-mta-1 Up 22 hours
...18 containers total
▸ analytics
● analytics-app-1 Up 23 hours
● analytics-db-1 Up 23 hours
● analytics-events-1 Up 23 hours
🔗 QUICK LINKS
──────────────────────────────────────────────
Webmail https://mail.example.com
Mail Admin https://mail.example.com/admin
Analytics https://stats.example.com
Monitoring http://localhost:19999 (local only)
🚀 DEPLOY COMMANDS
──────────────────────────────────────────────
deploy-mysite Deploy mysite
🛡 FIREWALL & SECURITY
──────────────────────────────────────────────
UFW ● active
UFW blocks 24 (last 24h)
fail2ban ● active (0 total bans)
SSH failed 0 (last 24h)
📦 PENDING UPDATES
──────────────────────────────────────────────
5 security + 0 other (5 total — run apt upgrade)
👤 LAST LOGIN
──────────────────────────────────────────────
user from 198.51.100.10 Mon Mar 9 08:20 - 09:25 (01:04)
The code block above is plain text — in a real terminal, everything is color-coded with ANSI escape sequences: green for healthy, yellow for warnings, red for problems. Load averages change color based on thresholds. RAM and disk bars fill proportionally. Docker containers show status dots. On my server I keep the security block visible even when things look healthy, because an explicit all-clear is useful too.
The entire thing is a single Bash script at /etc/update-motd.d/99-dashboard. It relies on standard tools like awk, df, free, and ss that ship with Ubuntu, plus whatever optional tools are on the machine (docker, ufw, fail2ban-client). The script checks for each tool before using it and skips sections gracefully when something is missing.
How Claude Code Builds the MOTD Dashboard
The key insight is that Claude Code does not need an inventory of the server up front. It can discover one.
When you give it the prompt, it runs through the server systematically:
- Reads
/proc/cpuinfoto identify the CPU model and core count - Runs
docker ps -ato find all containers and their compose projects - Scans Nginx configs in
/etc/nginx/sites-enabled/to find proxied domains - Checks
systemctlfor monitoring services like Netdata - Reads firewall state from UFW and fail2ban where available
- Scans
/usr/local/bin/deploy-*for deploy scripts - Checks
apt list --upgradableor the distro equivalent for pending updates
On Debian and Ubuntu, scripts in /etc/update-motd.d/ are executed by pam_motd as root during interactive logins (SSH, local console). They do not run for sudo, cron, or non-interactive sessions. The execution environment is restricted — pam_motd sanitizes PATH and environment variables, and AppArmor or SELinux policies may limit what the script can do. That gives the dashboard enough access to inspect Docker, services, and firewall state, but it also means the script has to be safe to run repeatedly, must use absolute paths for non-standard binaries, and must be fast enough not to make SSH feel sluggish.
Then Claude Code generates a Bash script that uses ANSI color codes and Unicode box-drawing characters, installs it, disables the default MOTD scripts, and tests it with run-parts.
The entire process — discovery, generation, testing, bug fixing — took about 15 minutes in a single Claude Code session.
The MOTD Dashboard Prompt for Claude Code
This is the prompt I wish I had when I started. Copy it, open Claude Code on a Debian or Ubuntu server, and paste it:
Scan this server and create a useful MOTD dashboard
at /etc/update-motd.d/99-dashboard.
Discover automatically:
- Hostname, IP, CPU model, RAM, disk, swap
- All Docker containers (grouped by compose project)
- Nginx or Apache virtual hosts and their domains
- Systemd services (databases, monitoring tools, etc.)
- Firewall status (ufw, iptables, or firewalld)
- fail2ban jails and current bans
- Pending apt/dnf/yum updates
- Deploy scripts in /usr/local/bin/deploy-*
- Network traffic stats on the main interface
- Last SSH login (previous session, not current)
Design:
- Use ANSI colors and Unicode box-drawing characters
- Color-coded thresholds: green (ok), yellow (warning), red (critical)
- Progress bars for RAM and disk usage
- Group Docker containers by compose project with status dots
- Add a Quick Links section for any web UIs you discover
- Add a security section, and hide optional sections only when they add no value
Steps:
1. Scan the server and gather all information
2. Create the dashboard script
3. Disable the default noisy scripts in /etc/update-motd.d/ as needed
4. chmod +x the new 99-dashboard script
5. Test with: run-parts --lsbsysinit /etc/update-motd.d/
6. Review the output for alignment issues, color problems, or bugs
7. Fix any issues and test again
The script must be safe to run on every login via pam_motd.
Assume a sanitized, non-interactive environment and use absolute paths where needed.
Avoid slow commands, password prompts, or anything that can block.
Claude Code will take it from there. It scans the server, discovers what is installed, generates a script tailored to that specific machine, and iterates until the output renders correctly.
While you are iterating in the same SSH session, you do not need to log out after every edit. Re-run the dashboard with sudo run-parts --lsbsysinit /etc/update-motd.d/ or execute just /etc/update-motd.d/99-dashboard directly to see the latest output.
Your dashboard will look different from mine because your server is different. That is the point.
What Happened on My Server
The prompt gets you a working dashboard, but not necessarily a perfect one on the first try. Here is what the iterative process looked like on my machine.
My server runs the same mail server stack as in my previous post (18 Docker containers), a web analytics platform (3 containers), Nginx as a reverse proxy for several domains, a monitoring agent, and fail2ban plus UFW. Claude Code found all of it without me listing a single service.
The process still needed a few iterations:
First pass: Claude Code generated the script, but the RAM bar was broken — it showed 1 block instead of 6 at 31% usage. The bug was subtle: in Bash, local pct=$1 width=20 filled=$(( pct * width / 100 )) expands the right-hand sides before the assignments in that local command take effect, so width is still unset when filled is computed. Splitting it into separate local lines fixed it.
Second pass: The header box was misaligned because the date string was wider than the format field Claude Code used. That only showed up once the script rendered a real timestamp in the terminal.
Third pass: The zombie process counter had a bug where grep -c returning zero (exit code 1) triggered the || echo 0 fallback, producing "0\n0" instead of "0" — which then broke the arithmetic comparison. Only visible when no zombies existed, which is exactly the kind of edge case a quick eyeball test misses.
Fourth pass: Port discovery missed IPv6 listeners because the regex only matched 0.0.0.0:PORT. Expanding it to handle :::PORT fixed the false negatives.
None of these bugs were dramatic. That is why they are interesting. They only surfaced when Claude Code rendered the actual dashboard, inspected the output, and iterated.
One extra gotcha is easy to miss when you are following along in the same SSH session: re-running the dashboard script is enough to test layout and content changes, but it does not refresh your shell’s group membership. If you add your user to the docker group during setup, the current shell will still behave as if nothing changed. You need a new login, or a command like newgrp docker, before docker ps reflects the new group membership. That is a session problem during interactive testing, not a property of the final MOTD script.
| Bug | Root Cause | Fix |
|---|---|---|
| RAM bar shows 1 block at 31% | local expands dependent values before the assignments in that command take effect | Separate local declarations |
| Header box misaligned | Date string wider than the format field | Adjust field widths |
| Zombie counter breaks with 0 zombies | grep -c exit code 1 triggers || echo 0, producing "0\n0" | || true + regex validation |
| Port display misses IPv6 | Regex only matched 0.0.0.0:PORT | Add :::PORT pattern |
A static tutorial would have shipped those bugs to every reader. An agentic workflow caught them because Claude Code generated the code, rendered the output, and then audited its own result.
Lessons Learned
pam_motd runs these hooks as root, but only on interactive login. On Debian and Ubuntu, scripts in /etc/update-motd.d/ are executed during SSH and console login by pam_motd. They do not run for sudo, cron, or SCP file transfers. That makes Docker and firewall inspection practical, but it also raises the bar: the script must be safe, non-interactive, and fast.
Test the real execution path. run-parts --lsbsysinit /etc/update-motd.d/ gets you close, but a real SSH login is still the final check because PAM sanitizes the environment (including PATH) and terminal rendering still matters. Measure overhead with time /etc/update-motd.d/99-dashboard — anything under 500ms is usually fine. If a section is too slow, cache its output to a file (e.g. via a cron job writing to /var/cache/motd-dashboard/) and read that file in the dashboard script instead of running the slow command on every login.
Re-run the script for content changes, re-login for group changes. sudo run-parts --lsbsysinit /etc/update-motd.d/ is enough to preview dashboard edits in the same session. But if you change group membership, for example by adding a user to the docker group, you still need a new login or newgrp docker before treating a Docker permission error as a real dashboard bug.
Bash local is not what it looks like. local a=1 b=$(( a + 1 )) does not do what the reader expects. If variables depend on each other, declare them on separate lines.
The discovery step is the real value. Writing a Bash script with ANSI colors is not the hard part. Figuring out which containers, domains, ports, and services belong in the dashboard is.
One prompt, different servers. The same prompt works on a mail server, a web server, or a small home-lab box because the agent reads the machine in front of it instead of filling in a fixed template.
Be careful what you expose. The MOTD is visible to every user who logs in. Avoid printing API keys, internal hostnames, or sensitive container names. If your Docker Compose project names or Nginx configs contain information you would not want a junior admin to screenshot, filter the output or restrict who can read the script.
Why not landscape-sysinfo or neofetch? Tools like landscape-sysinfo (ships with Ubuntu) and neofetch show system info out of the box, but they do not discover your Docker stacks, Nginx vhosts, deploy scripts, or firewall state. The value of the Claude Code approach is that the dashboard is tailored to what is actually running on your specific server. If you do not have access to Claude Code, you can still use this post as a reference for what sections to include and write the Bash script by hand — the dashboard design and the bug fixes described above apply regardless of how the script is authored.
To restore the defaults, re-enable the original scripts with chmod +x /etc/update-motd.d/10-* and remove the custom dashboard with rm /etc/update-motd.d/99-dashboard. Your next login will show the stock Ubuntu MOTD again.
Every server I manage will get some version of this custom SSH login dashboard from now on. Not because it is flashy, but because the first seconds after login should tell me whether the machine is healthy.
If the login banner cannot answer that, it is wasting space.
Frequently Asked Questions
Q: What is MOTD and why should I customize it?
A: MOTD stands for Message of the Day. It is the text you see when you log in to a Linux server. Replacing the default Ubuntu MOTD with a custom dashboard gives you an immediate view of system health, services, security status, and pending updates.
Q: Do /etc/update-motd.d scripts run as root?
A: On Debian and Ubuntu systems that use the dynamic MOTD framework, pam_motd executes executable scripts in /etc/update-motd.d as root during login. That makes system inspection easier, but it also means the script should be safe, non-interactive, and fast.
Q: Does this work on any Linux server?
A: The prompt is designed for Debian and Ubuntu servers (20.04+) that use /etc/update-motd.d. On Red Hat, Fedora, or Arch you can reuse the same idea, but the install path (/etc/motd or /etc/profile.d/), package manager, and available system tools differ.
Q: What is the difference between /etc/motd and /etc/update-motd.d?
A: /etc/motd is a static text file displayed on login. /etc/update-motd.d is a Debian/Ubuntu extension where pam_motd runs executable scripts to generate the MOTD dynamically on every login. This dashboard uses the dynamic approach.
Q: Will this slow down my SSH login?
A: A little. The dashboard runs on every login, so commands like docker, apt, or service discovery add overhead. Measure with time /etc/update-motd.d/99-dashboard. On a small VPS anything under 500ms is usually acceptable, but slow sections should be cached or trimmed.
Q: Can I modify the dashboard after Claude Code creates it?
A: Yes. The output is just a Bash script in /etc/update-motd.d/99-dashboard. You can edit it by hand, remove sections, or ask Claude Code to refine it in a later session.