# Spec: HTB-CLI
## Objective
A Rust CLI for interacting with the Hack The Box platform from the terminal. Query machines, challenges, seasons, and Sherlocks; spawn/stop instances; submit flags; manage VPN connections. Works with any HTB account (free or VIP).
Target users: pentesters and CTF players who prefer staying in the terminal over the web UI.
Success looks like: `htb machines list --os linux --difficulty easy` returns a colored table in under 2 seconds.
## Tech Stack
- **Language:** Rust (2021 edition)
- **CLI:** clap v4 (derive macros)
- **HTTP:** reqwest + rustls-tls
- **Async:** tokio (full features)
- **Errors:** thiserror (domain) + anyhow (binary boundary)
- **Config:** TOML via `toml` crate
- **Logging:** tracing + tracing-subscriber + EnvFilter
- **Serialization:** serde + serde_json + chrono
- **Output:** colored tables via `comfy-table`, `--json` flag for machine-readable output
- **Versioning:** knope (conventional commits + changesets, semver, changelog generation)
- **CI:** GitHub Actions
## Commands
### Auth
```
htb auth login # Prompt for API token, save to ~/.htb-cli/.token
htb auth status # Show current auth state and user info
htb auth logout # Remove stored token
```
### Machines
```
htb machines list # List active machines (table)
htb machines list --retired # Include retired machines
htb machines list --os linux # Filter by OS
htb machines list --difficulty easy # Filter by difficulty
htb machines info <name-or-id> # Detailed machine info
htb machines start <name-or-id> # Spawn machine
htb machines stop # Stop active machine
htb machines reset <name-or-id> # Reset machine
htb machines submit <name-or-id> <flag> # Submit flag (user or root)
htb machines active # Show currently active machine
htb machines todo # List todo machines
htb machines todo add <name-or-id> # Add to todo list
htb machines todo remove <name-or-id> # Remove from todo list
```
### Challenges
```
htb challenges list # List all challenges
htb challenges list --category <cat> # Filter by category
htb challenges categories # List categories
htb challenges info <slug> # Challenge details
htb challenges download <slug> # Download challenge files
htb challenges start <slug> # Start challenge instance
htb challenges stop <slug> # Stop challenge instance
htb challenges submit <id> <flag> # Submit flag
```
### Seasons
```
htb seasons list # List all seasons
htb seasons machines <season-id> # Machines in a season
htb seasons leaderboard <season-id> # Season leaderboard
htb seasons rank # Your rank in current season
```
### Sherlocks
```
htb sherlocks list # List all Sherlocks
htb sherlocks info <slug> # Sherlock details
htb sherlocks download <slug> # Download case files
htb sherlocks tasks <slug> # List tasks for a Sherlock
htb sherlocks submit <id> <task-id> <flag> # Submit task flag
```
### VPN
```
htb vpn status # Current connection status
htb vpn list # List available servers
htb vpn switch <server-id> # Switch VPN server
htb vpn download [server-id] # Download .ovpn file
htb vpn connect [server-id] # Download + start OpenVPN (optional feature)
htb vpn disconnect # Stop OpenVPN process (optional feature)
```
### User
```
htb user me # Your profile summary
htb user info <username-or-id> # Another user's profile
htb user activity # Your recent activity
```
### Search
```
htb search <query> # Global search across machines, challenges, users
```
### Global Flags
```
--json # Output as JSON instead of table
--no-color # Disable colored output
--verbose / -v # Enable debug logging (RUST_LOG=debug)
--config <path> # Override config file path
```
## Build / Dev Commands
```
cargo build # Debug build
cargo build --release # Release build
cargo test # Run all tests
cargo clippy -- -D warnings # Lint
cargo fmt --check # Format check
```
## Project Structure
```
htb-cli/
src/
main.rs # Entry point, clap setup, dispatch
cli/
mod.rs # Top-level CLI enum
machines.rs # Machine subcommands
challenges.rs # Challenge subcommands
seasons.rs # Season subcommands
sherlocks.rs # Sherlock subcommands
vpn.rs # VPN subcommands
user.rs # User subcommands
auth.rs # Auth subcommands
search.rs # Search subcommand
api/
mod.rs # HtbClient struct, shared request logic
machines.rs # Machine API calls
challenges.rs # Challenge API calls
seasons.rs # Season API calls
sherlocks.rs # Sherlock API calls
vpn.rs # VPN API calls
user.rs # User API calls
search.rs # Search API calls
models/
mod.rs # Re-exports
machine.rs # Machine response types
challenge.rs # Challenge response types
season.rs # Season response types
sherlock.rs # Sherlock response types
vpn.rs # VPN response types
user.rs # User response types
output/
mod.rs # OutputFormat enum, format dispatch
table.rs # Table rendering (comfy-table)
json.rs # JSON output
config.rs # Config loading (~/.htb-cli/config.toml)
error.rs # thiserror domain errors
tests/
api_integration.rs # Integration tests against mock server
.github/
workflows/
ci.yml # Clippy, test, fmt on PR/push
release.yml # knope release + binary builds
.changeset/ # knope changeset files
knope.toml # knope config
Cargo.toml
CHANGELOG.md
LICENSE
README.md
```
Single flat crate. Modules provide the separation; no workspace needed for a focused CLI tool.
## Code Style
```rust
use clap::{Parser, Subcommand};
use crate::api::HtbClient;
use crate::error::HtbError;
use crate::output::OutputFormat;
#[derive(Subcommand)]
pub enum MachineCommand {
List {
#[arg(long)]
retired: bool,
#[arg(long)]
os: Option<String>,
#[arg(long)]
difficulty: Option<String>,
},
Info {
name_or_id: String,
},
Start {
name_or_id: String,
},
Stop,
Reset {
name_or_id: String,
},
Submit {
name_or_id: String,
flag: String,
},
Active,
}
pub async fn handle(client: &HtbClient, cmd: MachineCommand, format: OutputFormat) -> anyhow::Result<()> {
match cmd {
MachineCommand::List { retired, os, difficulty } => {
let machines = client.machines().list(retired).await?;
// filter and output
}
// ...
}
Ok(())
}
```
- Derive macros for clap, serde
- One handler function per subcommand module
- `HtbClient` holds the reqwest client + base URL + auth token
- Domain errors via `HtbError` (thiserror), anyhow at the handler boundary
## API Client Design
```rust
pub struct HtbClient {
http: reqwest::Client,
base_url: String,
token: String,
}
impl HtbClient {
pub fn machines(&self) -> MachineApi<'_> { MachineApi(self) }
pub fn challenges(&self) -> ChallengeApi<'_> { ChallengeApi(self) }
pub fn seasons(&self) -> SeasonApi<'_> { SeasonApi(self) }
pub fn sherlocks(&self) -> SherlockApi<'_> { SherlockApi(self) }
pub fn vpn(&self) -> VpnApi<'_> { VpnApi(self) }
pub fn user(&self) -> UserApi<'_> { UserApi(self) }
}
```
Sub-API structs borrow the client. Each method maps 1:1 to an endpoint. Base URL: `https://labs.hackthebox.com/api/v4` (some endpoints use `/api/v5`).
## Rate Limiting
The API returns rate limit headers on every response:
- `x-ratelimit-limit`: max requests per window
- `x-ratelimit-remaining`: requests left in current window
Limits vary by endpoint (15-60 per window). The client tracks remaining quota
from response headers and delays requests when nearing the limit. No need to
spam the API when we know the ceiling.
```rust
pub struct RateLimitState {
remaining: AtomicU32,
limit: AtomicU32,
}
```
On each response, update from headers. Before each request, check remaining > 0.
If exhausted, wait and retry with backoff (reuse the exponential backoff pattern
from ridgeline's AzureDevOpsClient). Surface the limit to the user:
`Rate limited (14/25 remaining)` in verbose mode.
## Config
`~/.htb-cli/config.toml`:
```toml
# Default output format (table or json)
output = "table"
# Default VPN server ID
vpn_server = 1
# Disable colored output
no_color = false
```
Token stored separately at `~/.htb-cli/.token` (plaintext, 0o600 permissions).
## Testing Strategy
- **Unit tests:** serde deserialization of API responses (use recorded JSON fixtures in `tests/fixtures/`)
- **Integration tests:** mock HTTP server (wiremock-rs) to test full request/response cycle
- **CLI tests:** assert-cmd for end-to-end command parsing and output format
- **No live API tests in CI** (requires auth token)
## CI: GitHub Actions
### ci.yml (on push + PR)
1. Matrix: stable + nightly Rust, ubuntu-latest + macos-latest
2. Steps: checkout, cache, fmt check, clippy, test
3. Fail on warnings
### release.yml (on workflow_dispatch or knope-triggered tag)
1. Build release binaries (linux-x86_64, linux-aarch64, macos-x86_64, macos-aarch64)
2. Create GitHub release with binaries attached
3. Generate/update CHANGELOG.md via knope
### Knope workflow
- Developers create changeset files in `.changeset/` describing changes
- `knope release` bumps version in Cargo.toml, updates CHANGELOG.md, tags, creates GitHub release
- GitHub Actions runs `knope release` on dispatch
## Boundaries
- **Always:** Run clippy + tests before commits. Validate API responses (don't unwrap). Handle rate limiting gracefully.
- **Ask first:** Adding new content types beyond v1.0 scope. Changing output format defaults. Adding interactive prompts.
- **Never:** Store tokens in config.toml. Log tokens or API responses containing tokens. Make destructive API calls without confirmation (machine reset gets a prompt).
## Success Criteria
- `htb machines list` returns results in < 2s on a normal connection
- `htb machines info <name>` shows machine details with difficulty, OS, own status, blood times
- `htb machines start <name>` spawns the machine and reports the IP
- `htb challenges submit <id> <flag>` submits and shows result
- `--json` flag works on every list/info command
- `htb auth login` stores token and `htb auth status` confirms it works
- CI passes on every PR (clippy clean, tests green, fmt clean)
- knope produces a valid CHANGELOG.md entry on release
## Open Questions
1. Should `htb machines list` show paginated results or fetch all? The API supports `machine/paginated?per_page=100` but also non-paginated endpoints.
2. VPN connect/disconnect (managing the OpenVPN process) needs root/sudo on most systems. Should this shell out to `openvpn` or use a helper approach?
3. Should we support shell completions generation (`htb completions bash/zsh/fish`)?
4. The API base URL might differ between `www.hackthebox.com/api/v4` and `labs.hackthebox.com/api/v4`. Need to confirm which is current from your browser calls.
5. Are there any season-specific endpoints you use that aren't covered above?