Building a Microsoft Teams CLI: Reading JWT Tokens and Terminal UIs in Go
Hook
The official Microsoft Teams desktop client consumes over 700MB of RAM to display text messages. A Go-based terminal interface can do the same job in under 20MB.
Context
Microsoft Teams has become ubiquitous in enterprise development workflows, but its official client presents a frustrating paradox: it’s essential for collaboration yet notorious for resource consumption. Built on Electron, the Teams desktop client regularly consumes 500-800MB of RAM and significant CPU cycles—a hefty price for what’s fundamentally a chat application. For developers who spend their day in terminals, switching to a GUI just to check if someone mentioned you in a channel feels like context-switching overhead.
This friction is particularly acute in specific scenarios: SSH sessions into remote development machines, resource-constrained environments like older laptops or containers, or workflows where you’re already living in tmux with Vim and want Teams monitoring without Alt-Tabbing to a separate application. The teams-cli project addresses this gap by providing a terminal-based interface to Microsoft Teams, trading the polish of a GUI for the efficiency of keyboard-driven navigation and minimal resource footprint. It’s part of a broader trend of bringing enterprise SaaS tools into terminal-native workflows, similar to how projects like gh brought GitHub interaction to the command line.
Technical Insight
The architecture of teams-cli reveals interesting decisions about authentication, API interaction, and terminal UI design in Go. At its core, the project separates into three layers: authentication token management, API communication through the teams-api package, and the TUI presentation layer.
Authentication is handled through JWT tokens rather than OAuth flows, which is both a strength and a complexity. Users must first generate tokens using a separate teams-token tool that extracts authentication credentials from an existing Teams session. This approach bypasses the need for Microsoft to officially support third-party clients (which they don’t), but requires manual token refresh. The tokens are stored locally and loaded by teams-cli on startup:
// Token loading from the teams-api package
func LoadToken(tokenPath string) (*AuthToken, error) {
data, err := ioutil.ReadFile(tokenPath)
if err != nil {
return nil, fmt.Errorf("failed to read token file: %w", err)
}
var token AuthToken
if err := json.Unmarshal(data, &token); err != nil {
return nil, fmt.Errorf("failed to parse token: %w", err)
}
// Validate token expiration
if time.Now().After(token.ExpiresAt) {
return nil, fmt.Errorf("token expired at %v", token.ExpiresAt)
}
return &token, nil
}
The teams-api package handles all HTTP communication with Microsoft’s Teams backend. This separation is architecturally significant—it means teams-cli is essentially a thin presentation layer over a reusable API client. This enables other automation tools to leverage the same API package for scripting, webhooks, or custom integrations without reimplementing authentication and request handling.
The TUI itself uses a dual-pane layout pattern common in terminal applications like Midnight Commander or mutt. The left pane displays a hierarchical tree of teams, channels, and chats, while the right pane shows message content for the selected conversation. Navigation is keyboard-driven with Vim-like bindings—j/k for movement, Enter to select, and q to quit. The TUI implementation uses Go’s concurrency primitives to handle background refresh without blocking user interaction:
// Simplified refresh mechanism
type App struct {
conversations ConversationTree
messages MessageView
refreshTicker *time.Ticker
updateChan chan Update
}
func (a *App) StartBackgroundRefresh() {
a.refreshTicker = time.NewTicker(30 * time.Second)
go func() {
for range a.refreshTicker.C {
updates, err := a.client.FetchNewMessages()
if err != nil {
log.Printf("refresh error: %v", err)
continue
}
a.updateChan <- Update{Messages: updates}
}
}()
}
This goroutine-based refresh pattern is idiomatic Go: a ticker fires every 30 seconds, fetches new messages in a background goroutine, and sends updates through a channel that the main UI loop consumes. This prevents the UI from freezing during network requests while keeping conversations synchronized with the server.
One clever implementation detail is the ‘doctor’ command, which diagnoses common setup issues. It validates token format, checks expiration, verifies network connectivity to Teams endpoints, and confirms API accessibility. This is excellent UX engineering—CLI tools often fail silently or with cryptic errors, but doctor provides actionable debugging information:
$ teams-cli doctor
✓ Token file found at ~/.config/teams-cli/token.json
✓ Token format valid
✓ Token expires in 6 hours
✓ Network connectivity to teams.microsoft.com
✗ API test failed: 401 Unauthorized
→ Token may be expired or invalid
→ Run 'teams-token' to generate a new token
The read-only nature of the current implementation reflects a pragmatic development strategy. Reading messages requires only GET requests to Teams APIs, while sending messages involves more complex CRUD operations, message formatting, and real-time synchronization. By focusing on read operations first, the project delivers immediate value (monitoring channels) while building toward the more complex write functionality.
Gotcha
The most significant limitation is the read-only restriction. You can browse teams, channels, and chats, and you can read messages, but you cannot send messages, react to messages, upload files, or perform any write operations. This fundamentally limits teams-cli to a monitoring role rather than a full client replacement. If your workflow involves active participation in Teams conversations throughout the day, you’ll still need the official client running alongside teams-cli.
The manual token management process introduces both friction and security considerations. Extracting JWT tokens from an existing Teams session isn’t straightforward for non-technical users, and tokens expire requiring periodic regeneration. There’s no automatic token refresh mechanism, so you’ll encounter authentication failures that require running teams-token again. From a security perspective, storing long-lived JWT tokens in plaintext on disk (even in a user-only readable file) is less secure than OAuth flows with short-lived tokens and refresh mechanisms. Organizations with strict security policies may prohibit this approach entirely. Additionally, because this isn’t an officially supported Microsoft client, there’s always a risk that Teams API changes could break functionality without warning—this is community-maintained tooling for an undocumented API surface.
Verdict
Use teams-cli if you live in the terminal and need lightweight monitoring of Teams channels without the resource overhead of the official client—ideal for checking mentions, monitoring deployment channels during on-call rotations, or running on remote servers via SSH. It’s particularly valuable in resource-constrained environments where every 500MB of RAM matters, or when you’re already running a terminal multiplexer and want Teams visibility without context switching to a GUI. Skip it if you need to actively participate in conversations beyond reading (no message sending yet), if your organization’s security policies prohibit unofficial API clients, or if you’re uncomfortable with manual JWT token extraction and management. Also skip if you prefer mouse-driven interfaces or need Teams features beyond basic messaging like video calls, screen sharing, or file collaboration. This is a monitoring tool first, not yet a full client replacement.