diff --git a/go.mod b/go.mod
index 6ebc1b8..e5fb523 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/cilium/statedb
 go 1.23
 
 require (
-	github.com/cilium/hive v0.0.0-20241025140746-d66ad09f4384
+	github.com/cilium/hive v0.0.0-20241213121623-605c1412b9b3
 	github.com/cilium/stream v0.0.0-20240209152734-a0792b51812d
 	github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de
 	github.com/spf13/cobra v1.8.0
diff --git a/go.sum b/go.sum
index b92c418..f095e82 100644
--- a/go.sum
+++ b/go.sum
@@ -1,7 +1,7 @@
-github.com/cilium/hive v0.0.0-20241011093954-8df06c41a157 h1:8UuDJ7JPPoCaDfZ/WkU/aP3FtNCwdNQe+7fbzP+lZrk=
-github.com/cilium/hive v0.0.0-20241011093954-8df06c41a157/go.mod h1:pI2GJ1n3SLKIQVFrKF7W6A6gb6BQkZ+3Hp4PAEo5SuI=
-github.com/cilium/hive v0.0.0-20241025140746-d66ad09f4384 h1:MAkG2lk4v0Z8J2X4+fFnhuCEsIJGPdCCrWzL41S2Z/I=
-github.com/cilium/hive v0.0.0-20241025140746-d66ad09f4384/go.mod h1:pI2GJ1n3SLKIQVFrKF7W6A6gb6BQkZ+3Hp4PAEo5SuI=
+github.com/cilium/hive v0.0.0-20241213101835-553aca42f74a h1:KuDVdRWFhuntkXMuXBraKvsJ4o6HuPf3iF2hETefRtE=
+github.com/cilium/hive v0.0.0-20241213101835-553aca42f74a/go.mod h1:pI2GJ1n3SLKIQVFrKF7W6A6gb6BQkZ+3Hp4PAEo5SuI=
+github.com/cilium/hive v0.0.0-20241213121623-605c1412b9b3 h1:RfmUH1ouzj0LzORYJRhp43e1rlGpx6GNv4NIRUakU2w=
+github.com/cilium/hive v0.0.0-20241213121623-605c1412b9b3/go.mod h1:pI2GJ1n3SLKIQVFrKF7W6A6gb6BQkZ+3Hp4PAEo5SuI=
 github.com/cilium/stream v0.0.0-20240209152734-a0792b51812d h1:p6MgATaKEB9o7iAsk9rlzXNDMNCeKPAkx4Y8f+Zq8X8=
 github.com/cilium/stream v0.0.0-20240209152734-a0792b51812d/go.mod h1:3VLiLgs8wfjirkuYqos4t0IBPQ+sXtf3tFkChLm6ARM=
 github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
diff --git a/reconciler/testdata/batching.txtar b/reconciler/testdata/batching.txtar
index 4dc5835..efa21be 100644
--- a/reconciler/testdata/batching.txtar
+++ b/reconciler/testdata/batching.txtar
@@ -49,11 +49,11 @@ health 'job-reconcile.*level=OK.*message=OK, 0 object'
 
 # Check metrics
 expvar
-! grep 'reconciliation_count.test: 0$'
-grep 'reconciliation_current_errors.test: 0$'
-! grep 'reconciliation_total_errors.test: 0$'
-! grep 'reconciliation_duration.test/update: 0$'
-! grep 'reconciliation_duration.test/delete: 0$'
+! stdout 'reconciliation_count.test: 0$'
+stdout 'reconciliation_current_errors.test: 0$'
+! stdout 'reconciliation_total_errors.test: 0$'
+! stdout 'reconciliation_duration.test/update: 0$'
+! stdout 'reconciliation_duration.test/delete: 0$'
 
 # ------------
 
diff --git a/reconciler/testdata/incremental.txtar b/reconciler/testdata/incremental.txtar
index f48f4b6..6d9226f 100644
--- a/reconciler/testdata/incremental.txtar
+++ b/reconciler/testdata/incremental.txtar
@@ -48,11 +48,11 @@ health 'job-reconcile.*level=OK.*message=OK, 0 object'
 
 # Check metrics
 expvar
-! grep 'reconciliation_count.test: 0$'
-grep 'reconciliation_current_errors.test: 0$'
-! grep 'reconciliation_total_errors.test: 0$'
-! grep 'reconciliation_duration.test/update: 0$'
-! grep 'reconciliation_duration.test/delete: 0$'
+! stdout 'reconciliation_count.test: 0$'
+stdout 'reconciliation_current_errors.test: 0$'
+! stdout 'reconciliation_total_errors.test: 0$'
+! stdout 'reconciliation_duration.test/update: 0$'
+! stdout 'reconciliation_duration.test/delete: 0$'
 
 # ------------
 
diff --git a/reconciler/testdata/pruning.txtar b/reconciler/testdata/pruning.txtar
index 431373d..533cc5f 100644
--- a/reconciler/testdata/pruning.txtar
+++ b/reconciler/testdata/pruning.txtar
@@ -14,7 +14,7 @@ mark-init
 expect-ops prune(n=2)
 health 'job-reconcile.*level=OK'
 expvar
-! grep 'prune_count.test: 0'
+! stdout  'prune_count.test: 0'
 
 # Pruning with faulty ops will mark status as degraded
 set-faulty true
@@ -22,7 +22,7 @@ prune
 expect-ops 'prune(n=2) fail'
 health 'job-reconcile.*level=Degraded.*message=.*prune fail'
 expvar
-grep 'prune_current_errors.test: 1'
+stdout 'prune_current_errors.test: 1'
 
 # Pruning again with healthy ops fixes the status.
 set-faulty false
@@ -30,7 +30,7 @@ prune
 expect-ops 'prune(n=2)'
 health 'job-reconcile.*level=OK'
 expvar
-grep 'prune_current_errors.test: 0'
+stdout 'prune_current_errors.test: 0'
 
 # Delete an object and check pruning happens without it
 db/delete test-objects obj1.yaml
@@ -44,15 +44,15 @@ expect-ops prune(n=0) delete(2) prune(n=1)
 
 # Check metrics
 expvar
-! grep 'prune_count.test: 0'
-grep 'prune_current_errors.test: 0'
-grep 'prune_total_errors.test: 1'
-! grep 'prune_duration.test: 0$'
-! grep 'reconciliation_count.test: 0$'
-grep 'reconciliation_current_errors.test: 0$'
-grep 'reconciliation_total_errors.test: 0$'
-! grep 'reconciliation_duration.test/update: 0$'
-! grep 'reconciliation_duration.test/delete: 0$'
+! stdout 'prune_count.test: 0'
+stdout 'prune_current_errors.test: 0'
+stdout 'prune_total_errors.test: 1'
+! stdout 'prune_duration.test: 0$'
+! stdout 'reconciliation_count.test: 0$'
+stdout 'reconciliation_current_errors.test: 0$'
+stdout 'reconciliation_total_errors.test: 0$'
+! stdout 'reconciliation_duration.test/update: 0$'
+! stdout 'reconciliation_duration.test/delete: 0$'
 
 -- obj1.yaml --
 id: 1
diff --git a/script.go b/script.go
index 6e94b27..c0e805f 100644
--- a/script.go
+++ b/script.go
@@ -6,7 +6,6 @@ package statedb
 import (
 	"bytes"
 	"encoding/json"
-	"flag"
 	"fmt"
 	"io"
 	"iter"
@@ -19,6 +18,7 @@ import (
 	"github.com/cilium/hive"
 	"github.com/cilium/hive/script"
 	"github.com/liggitt/tabwriter"
+	"github.com/spf13/pflag"
 	"golang.org/x/time/rate"
 	"gopkg.in/yaml.v3"
 )
@@ -90,17 +90,14 @@ func DBCmd(db *DB) script.Cmd {
 	)
 }
 
-func newCmdFlagSet(w io.Writer) *flag.FlagSet {
-	fs := flag.NewFlagSet("", flag.ContinueOnError)
-	fs.SetOutput(w)
-	return fs
-}
-
 func InitializedCmd(db *DB) script.Cmd {
 	return script.Command(
 		script.CmdUsage{
 			Summary: "Wait until all or specific tables have been initialized",
-			Args:    "[-timeout=<duration>] table...",
+			Args:    "table...",
+			Flags: func(fs *pflag.FlagSet) {
+				fs.Duration("timeout", 5*time.Second, "Maximum amount of time to wait for the table contents to match")
+			},
 			Detail: []string{
 				"Waits until all or specific tables have been marked",
 				"initialized. The default timeout is 5 seconds.",
@@ -111,15 +108,13 @@ func InitializedCmd(db *DB) script.Cmd {
 			},
 		},
 		func(s *script.State, args ...string) (script.WaitFunc, error) {
-			flags := newCmdFlagSet(s.LogWriter())
-			timeout := flags.Duration("timeout", 5*time.Second, "Maximum amount of time to wait for the table contents to match")
-			if err := flags.Parse(args); err != nil {
-				return nil, fmt.Errorf("%w: %w", script.ErrUsage, err)
+			timeout, err := s.Flags.GetDuration("timeout")
+			if err != nil {
+				return nil, err
 			}
-			args = flags.Args()
 
 			txn := db.ReadTxn()
-			timeoutChan := time.After(*timeout)
+			timeoutChan := time.After(timeout)
 			allTbls := db.GetTables(txn)
 			tbls := allTbls
 
@@ -166,7 +161,12 @@ func ShowCmd(db *DB) script.Cmd {
 	return script.Command(
 		script.CmdUsage{
 			Summary: "Show the contents of a table",
-			Args:    "[-o=<file>] [-columns=col1,...] [-format={table,yaml,json}] table",
+			Args:    "table",
+			Flags: func(fs *pflag.FlagSet) {
+				fs.StringP("out", "o", "", "File to write to instead of stdout")
+				fs.StringSlice("columns", nil, "Columns to write")
+				fs.StringP("format", "f", "table", "Format to write in (table, yaml or json)")
+			},
 			Detail: []string{
 				"Show the contents of a table.",
 				"",
@@ -182,20 +182,19 @@ func ShowCmd(db *DB) script.Cmd {
 			},
 		},
 		func(s *script.State, args ...string) (script.WaitFunc, error) {
-			flags := newCmdFlagSet(s.LogWriter())
-			file := flags.String("o", "", "File to write to instead of stdout")
-			columns := flags.String("columns", "", "Comma-separated list of columns to write")
-			format := flags.String("format", "table", "Format to write in (table, yaml, json)")
-			if err := flags.Parse(args); err != nil {
-				return nil, fmt.Errorf("%w: %w", script.ErrUsage, err)
+			file, err := s.Flags.GetString("out")
+			if err != nil {
+				return nil, err
 			}
-
-			var cols []string
-			if len(*columns) > 0 {
-				cols = strings.Split(*columns, ",")
+			columns, err := s.Flags.GetStringSlice("columns")
+			if err != nil {
+				return nil, err
+			}
+			format, err := s.Flags.GetString("format")
+			if err != nil {
+				return nil, err
 			}
 
-			args = flags.Args()
 			if len(args) < 1 {
 				return nil, fmt.Errorf("missing table name")
 			}
@@ -203,12 +202,12 @@ func ShowCmd(db *DB) script.Cmd {
 			return func(*script.State) (stdout, stderr string, err error) {
 				var buf strings.Builder
 				var w io.Writer
-				if *file == "" {
+				if file == "" {
 					w = &buf
 				} else {
-					f, err := os.OpenFile(s.Path(*file), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
+					f, err := os.OpenFile(s.Path(file), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
 					if err != nil {
-						return "", "", fmt.Errorf("OpenFile(%s): %w", *file, err)
+						return "", "", fmt.Errorf("OpenFile(%s): %w", file, err)
 					}
 					defer f.Close()
 					w = f
@@ -217,7 +216,7 @@ func ShowCmd(db *DB) script.Cmd {
 				if err != nil {
 					return "", "", err
 				}
-				err = writeObjects(tbl, tbl.All(txn), w, cols, *format)
+				err = writeObjects(tbl, tbl.All(txn), w, columns, format)
 				return buf.String(), "", err
 			}, nil
 		})
@@ -227,7 +226,11 @@ func CompareCmd(db *DB) script.Cmd {
 	return script.Command(
 		script.CmdUsage{
 			Summary: "Compare table",
-			Args:    "[-timeout=<dur>] [-grep=<pattern>] table file",
+			Args:    "table file",
+			Flags: func(fs *pflag.FlagSet) {
+				fs.Duration("timeout", 5*time.Second, "Maximum amount of time to wait for the table contents to match")
+				fs.String("grep", "", "Grep the result rows and only compare matching ones")
+			},
 			Detail: []string{
 				"Compare the contents of a table against a file.",
 				"The comparison is retried until a timeout (1s default).",
@@ -243,21 +246,22 @@ func CompareCmd(db *DB) script.Cmd {
 			},
 		},
 		func(s *script.State, args ...string) (script.WaitFunc, error) {
-			flags := newCmdFlagSet(s.LogWriter())
-			timeout := flags.Duration("timeout", time.Second, "Maximum amount of time to wait for the table contents to match")
-			grep := flags.String("grep", "", "Grep the result rows and only compare matching ones")
-			err := flags.Parse(args)
+			timeout, err := s.Flags.GetDuration("timeout")
+			if err != nil {
+				return nil, err
+			}
+			grep, err := s.Flags.GetString("grep")
 			if err != nil {
-				return nil, fmt.Errorf("%w: %w", script.ErrUsage, err)
+				return nil, err
 			}
-			args = flags.Args()
+
 			if len(args) != 2 {
 				return nil, fmt.Errorf("expected table and filename")
 			}
 
 			var grepRe *regexp.Regexp
-			if *grep != "" {
-				grepRe, err = regexp.Compile(*grep)
+			if grep != "" {
+				grepRe, err = regexp.Compile(grep)
 				if err != nil {
 					return nil, fmt.Errorf("bad grep: %w", err)
 				}
@@ -292,7 +296,7 @@ func CompareCmd(db *DB) script.Cmd {
 			}
 			lines = lines[1:]
 			origLines := lines
-			timeoutChan := time.After(*timeout)
+			timeoutChan := time.After(timeout)
 
 			for {
 				lines = origLines
@@ -485,8 +489,15 @@ func queryCmd(db *DB, query int, summary string, detail []string) script.Cmd {
 	return script.Command(
 		script.CmdUsage{
 			Summary: summary,
-			Args:    "[-o=<file>] [-columns=col1,...] [-format={table*,yaml,json}] [-index=<index>] table key",
-			Detail:  detail,
+			Args:    "table key",
+			Flags: func(fs *pflag.FlagSet) {
+				fs.StringP("out", "o", "", "File to write to instead of stdout")
+				fs.StringSlice("columns", nil, "Columns to write")
+				fs.StringP("format", "f", "table", "Format to write in (table, yaml or json)")
+				fs.StringP("index", "i", "", "Index to query")
+				fs.Bool("delete", false, "Delete all matching objects")
+			},
+			Detail: detail,
 		},
 		func(s *script.State, args ...string) (script.WaitFunc, error) {
 			return runQueryCmd(query, db, s, args)
@@ -495,22 +506,27 @@ func queryCmd(db *DB, query int, summary string, detail []string) script.Cmd {
 }
 
 func runQueryCmd(query int, db *DB, s *script.State, args []string) (script.WaitFunc, error) {
-	flags := newCmdFlagSet(s.LogWriter())
-	file := flags.String("o", "", "File to write results to instead of stdout")
-	index := flags.String("index", "", "Index to query")
-	format := flags.String("format", "table", "Format to write in (table, yaml, json)")
-	columns := flags.String("columns", "", "Comma-separated list of columns to write")
-	delete := flags.Bool("delete", false, "Delete all matching objects")
-	if err := flags.Parse(args); err != nil {
-		return nil, fmt.Errorf("%w: %w", script.ErrUsage, err)
+	file, err := s.Flags.GetString("out")
+	if err != nil {
+		return nil, err
 	}
-
-	var cols []string
-	if len(*columns) > 0 {
-		cols = strings.Split(*columns, ",")
+	columns, err := s.Flags.GetStringSlice("columns")
+	if err != nil {
+		return nil, err
+	}
+	format, err := s.Flags.GetString("format")
+	if err != nil {
+		return nil, err
+	}
+	index, err := s.Flags.GetString("index")
+	if err != nil {
+		return nil, err
+	}
+	delete, err := s.Flags.GetBool("delete")
+	if err != nil {
+		return nil, err
 	}
 
-	args = flags.Args()
 	if len(args) < 2 {
 		return nil, fmt.Errorf("expected table and key")
 	}
@@ -523,12 +539,12 @@ func runQueryCmd(query int, db *DB, s *script.State, args []string) (script.Wait
 
 		var buf strings.Builder
 		var w io.Writer
-		if *file == "" {
+		if file == "" {
 			w = &buf
 		} else {
-			f, err := os.OpenFile(s.Path(*file), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
+			f, err := os.OpenFile(s.Path(file), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
 			if err != nil {
-				return "", "", fmt.Errorf("OpenFile(%s): %s", *file, err)
+				return "", "", fmt.Errorf("OpenFile(%s): %s", file, err)
 			}
 			defer f.Close()
 			w = f
@@ -537,13 +553,13 @@ func runQueryCmd(query int, db *DB, s *script.State, args []string) (script.Wait
 		var it iter.Seq2[any, uint64]
 		switch query {
 		case queryCmdList:
-			it, err = tbl.List(txn, *index, args[1])
+			it, err = tbl.List(txn, index, args[1])
 		case queryCmdLowerBound:
-			it, err = tbl.LowerBound(txn, *index, args[1])
+			it, err = tbl.LowerBound(txn, index, args[1])
 		case queryCmdPrefix:
-			it, err = tbl.Prefix(txn, *index, args[1])
+			it, err = tbl.Prefix(txn, index, args[1])
 		case queryCmdGet:
-			it, err = tbl.List(txn, *index, args[1])
+			it, err = tbl.List(txn, index, args[1])
 			if err == nil {
 				it = firstOfSeq2(it)
 			}
@@ -554,12 +570,12 @@ func runQueryCmd(query int, db *DB, s *script.State, args []string) (script.Wait
 			return "", "", fmt.Errorf("query: %w", err)
 		}
 
-		err = writeObjects(tbl, it, w, cols, *format)
+		err = writeObjects(tbl, it, w, columns, format)
 		if err != nil {
 			return "", "", err
 		}
 
-		if *delete {
+		if delete {
 			wtxn := db.WriteTxn(tbl.Meta)
 			count := 0
 			for obj := range it {
diff --git a/testdata/db.txtar b/testdata/db.txtar
index 286e29d..d48d5bc 100644
--- a/testdata/db.txtar
+++ b/testdata/db.txtar
@@ -24,44 +24,44 @@ db/insert test2 obj2.yaml
 
 # Show (non-empty)
 db/show test1
-grep ^ID.*Tags
-grep 1.*bar
-grep 2.*baz
+stdout ^ID.*Tags
+stdout 1.*bar
+stdout 2.*baz
 db/show test2
 
-db/show -format=table test1
-grep ^ID.*Tags
-grep 1.*bar
-grep 2.*baz
+db/show --format=table test1
+stdout ^ID.*Tags
+stdout 1.*bar
+stdout 2.*baz
 
-db/show -format=table -columns=Tags test1
-grep ^Tags$
-grep '^bar, foo$'
-grep '^baz, foo$'
+db/show --format=table --columns=Tags test1
+stdout ^Tags$
+stdout '^bar, foo$'
+stdout '^baz, foo$'
 
-db/show -format=table -o=test1_show.table test1
+db/show -f table -o test1_show.table test1
 cmp test1.table test1_show.table
 
-db/show -format=yaml -o=test1_show.yaml test1
+db/show --format=yaml --out=test1_show.yaml test1
 cmp test1.yaml test1_show.yaml
 
-db/show -format=json -o=test1_show.json test1
+db/show --format=json -o=test1_show.json test1
 cmp test1.json test1_show.json
 
 # Get
 db/get test2 2
-db/get -format=table test2 2
-grep '^ID.*Tags$'
-grep ^2.*baz
-db/get -format=table -columns=Tags test2 2
-grep ^Tags$
-grep '^baz, foo$'
-db/get -format=json test2 2
-db/get -format=yaml test2 2
-db/get -format=yaml -o=obj2_get.yaml test2 2
+db/get --format=table test2 2
+stdout '^ID.*Tags$'
+stdout ^2.*baz
+db/get --format=table --columns=Tags test2 2
+stdout ^Tags$
+stdout '^baz, foo$'
+db/get --format=json test2 2
+db/get --format=yaml test2 2
+db/get --format=yaml -o=obj2_get.yaml test2 2
 cmp obj2.yaml obj2_get.yaml
 
-db/get -index=tags -format=yaml -o=obj1_get.yaml test1 bar
+db/get -i tags -f yaml -o obj1_get.yaml test1 bar
 cmp obj1.yaml obj1_get.yaml
 
 # List
@@ -70,24 +70,24 @@ cmp obj1.table list.table
 db/list -o=list.table test1 2
 cmp obj2.table list.table
 
-db/list -o=list.table -index=tags test1 bar
+db/list -o list.table -i tags test1 bar
 cmp obj1.table list.table
-db/list -o=list.table -index=tags test1 baz
+db/list -o=list.table -i=tags test1 baz
 cmp obj2.table list.table
-db/list -o=list.table -index=tags test1 foo
+db/list --out=list.table --index=tags test1 foo
 cmp objs.table list.table
 
-db/list -format=table -index=tags -columns=Tags test1 foo
-grep ^Tags$
-grep '^bar, foo$'
-grep '^baz, foo$'
+db/list --format=table --index=tags --columns=Tags test1 foo
+stdout ^Tags$
+stdout '^bar, foo$'
+stdout '^baz, foo$'
 
 # Prefix
 # uint64 so can't really prefix search meaningfully, unless
 # FromString() accomodates partial keys.
 db/prefix test1 1
 
-db/prefix -o=prefix.table -index=tags test1 ba
+db/prefix -o=prefix.table --index=tags test1 ba
 cmp objs.table prefix.table
 
 # LowerBound
@@ -103,8 +103,8 @@ cmp empty.table lb.table
 # Compare
 db/cmp test1 objs.table
 db/cmp test1 objs_ids.table
-db/cmp -grep=bar test1 obj1.table
-db/cmp -grep=baz test1 obj2.table
+db/cmp --grep=bar test1 obj1.table
+db/cmp --grep=baz test1 obj2.table
 
 # Delete
 db/delete test1 obj1.yaml
@@ -116,21 +116,21 @@ db/cmp test1 empty.table
 # Delete with get
 db/insert test1 obj1.yaml
 db/cmp test1 obj1.table
-db/get -delete test1 1
+db/get --delete test1 1
 db/cmp test1 empty.table
 
 # Delete with prefix
 db/insert test1 obj1.yaml
 db/insert test1 obj2.yaml
 db/cmp test1 objs.table
-db/prefix -index=tags -delete test1 fo
+db/prefix --index=tags --delete test1 fo
 db/cmp test1 empty.table
 
 # Delete with lowerbound
 db/insert test1 obj1.yaml
 db/insert test1 obj2.yaml
 db/cmp test1 objs.table
-db/lowerbound -index=id -delete test1 2
+db/lowerbound --index=id --delete test1 2
 db/cmp test1 obj1.table
 
 # Tables