Index: /trunk/cmd/jouncectl/main.go
===================================================================
--- /trunk/cmd/jouncectl/main.go	(revision 84)
+++ /trunk/cmd/jouncectl/main.go	(revision 84)
@@ -0,0 +1,81 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"log"
+	"os"
+
+	"git.sr.ht/~emersion/jounce"
+	"git.sr.ht/~emersion/jounce/config"
+	"golang.org/x/crypto/bcrypt"
+	"golang.org/x/crypto/ssh/terminal"
+)
+
+const usage = `usage: jouncectl [-config path] <action> [options...]
+
+  create-user <username>  Create a new user
+  help                    Show this help message
+`
+
+func init() {
+	flag.Usage = func() {
+		fmt.Fprintf(flag.CommandLine.Output(), usage)
+	}
+}
+
+func main() {
+	var configPath string
+	flag.StringVar(&configPath, "config", "", "path to configuration file")
+	flag.Parse()
+
+	var cfg *config.Server
+	if configPath != "" {
+		var err error
+		cfg, err = config.Load(configPath)
+		if err != nil {
+			log.Fatalf("failed to load config file: %v", err)
+		}
+	} else {
+		cfg = config.Defaults()
+	}
+
+	db, err := jounce.OpenSQLDB(cfg.SQLDriver, cfg.SQLSource)
+	if err != nil {
+		log.Fatalf("failed to open database: %v", err)
+	}
+
+	switch cmd := flag.Arg(0); cmd {
+	case "create-user":
+		username := flag.Arg(1)
+		if username == "" {
+			flag.Usage()
+			os.Exit(1)
+		}
+
+		fmt.Printf("Password: ")
+		password, err := terminal.ReadPassword(int(os.Stdin.Fd()))
+		if err != nil {
+			log.Fatalf("failed to read password: %v", err)
+		}
+		fmt.Printf("\n")
+
+		hashed, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost)
+		if err != nil {
+			log.Fatalf("failed to hash password: %v", err)
+		}
+
+		user := jounce.User{
+			Username: username,
+			Password: string(hashed),
+		}
+		if err := db.CreateUser(&user); err != nil {
+			log.Fatalf("failed to create user: %v", err)
+		}
+	default:
+		flag.Usage()
+		if cmd != "help" {
+			os.Exit(1)
+		}
+	}
+}
Index: /trunk/db.go
===================================================================
--- /trunk/db.go	(revision 83)
+++ /trunk/db.go	(revision 84)
@@ -74,4 +74,26 @@
 }
 
+func (db *DB) CreateUser(user *User) error {
+	db.lock.Lock()
+	defer db.lock.Unlock()
+
+	tx, err := db.db.Begin()
+	if err != nil {
+		return err
+	}
+	defer tx.Rollback()
+
+	var password *string
+	if user.Password != "" {
+		password = &user.Password
+	}
+	_, err = tx.Exec("INSERT INTO User(username, password) VALUES (?, ?)", user.Username, password)
+	if err != nil {
+		return err
+	}
+
+	return tx.Commit()
+}
+
 func (db *DB) ListNetworks(username string) ([]Network, error) {
 	db.lock.RLock()
Index: /trunk/go.mod
===================================================================
--- /trunk/go.mod	(revision 83)
+++ /trunk/go.mod	(revision 84)
@@ -5,4 +5,5 @@
 require (
 	github.com/mattn/go-sqlite3 v2.0.3+incompatible
+	golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4
 	gopkg.in/irc.v3 v3.1.0
 )
Index: /trunk/go.sum
===================================================================
--- /trunk/go.sum	(revision 83)
+++ /trunk/go.sum	(revision 84)
@@ -2,4 +2,12 @@
 github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
 github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4 h1:QmwruyY+bKbDDL0BaglrbZABEali68eoMFhTZpCjYVA=
+golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 gopkg.in/irc.v3 v3.1.0 h1:AeDaEhQ/78gHfpbj/3mSi8FfiNIsFiVrWEgLzOwHWnU=
 gopkg.in/irc.v3 v3.1.0/go.mod h1:qE0DWv0j8Z8wCbFhA9783JBO0bufi3rttcV1Sjin8io=
