Repository: gopasspw/gopass

Issue:
age agent does not recover after being locked
When the agent is enabled it will ask to be unlocked at first usage. Works fine.
Then it can be locked by `gopass age lock`. Works fine.
But afterwards the agent will stay locked and doesn't unlock automatically (and there is no command for it).

We should fix both, i.e. resume the agent after the user has entered the passphrase again and provide a dedicated command to do so.

And while at it: We should extend the output of `gopass age agent status` to show if it is locked and it is confusing that the `lock` command is below `gopass age` while the other agent commands are under `gopass age agent`. I think we should provide an alias. And `gopass age agent shouldn't start the agent. We should call it `gopass age agent start` and `gopass age agent` should simply show the available subcommands.

Discussion:

Solution patch:
diff --git a/.golangci.yml b/.golangci.yml
index ae828850..0b01d3ac 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -62,7 +62,7 @@ linters:
     - zerologlint
   settings:
     cyclop:
-      max-complexity: 22
+      max-complexity: 24
     errcheck:
       exclude-functions:
         - fmt.Fprint
diff --git a/docs/config.md b/docs/config.md
index 9795ffaa..c4726be3 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -84,6 +84,7 @@ This is a list of available options:
 | **Option**                      | **Type** | Description                                                                                                                                                                                                                        | _Default_                           |
 | ------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- |
 | `age.agent-enabled`             | `bool`   | Enable the persistent Agent for caching of age identities. This will remove the need to repedately enter the passphrase. EXPERIMENTAL. |
+| `age.agent-timeout` | `int` | Automatically lock the agent after this many seconds of inactivity. | `0` |
 | `age.ssh-key-path`              | `string` | Path to a custom SSH key file or directory containing SSH keys. If set, this will be used to load SSH identities for age. If not set, the default SSH directory is used.                                                           | `~/.ssh`                            |
 | `age.usekeychain`               | `bool`   | Use the OS keychain to cache age passphrases.                                                                                                                                                                                      | `false`                             |
 | `audit.concurrency`             | `int`    | Number of concurrent audit workers.                                                                                                                                                                                                | ``                                  |
diff --git a/internal/action/delete.go b/internal/action/delete.go
index 8654e163..44cb0c6b 100644
--- a/internal/action/delete.go
+++ b/internal/action/delete.go
@@ -36,6 +36,7 @@ func (s *Action) Delete(c *cli.Context) error {
 	if s.Store.IsDir(ctx, name) && !s.Store.Exists(ctx, name) {
 		return exit.Error(exit.Usage, nil, "Cannot remove %q: Is a directory. Use 'gopass rm -r %s' to delete", name, name)
 	}
+
 	// specifying a key is optional.
 	key := c.Args().Get(1)
 
@@ -57,11 +58,6 @@ func (s *Action) Delete(c *cli.Context) error {
 	}
 	ctx = ctxutil.WithCommitMessage(ctx, commitMsg)
 
-	// multiple secrets, so not a key
-	if len(c.Args().Tail()) > 1 {
-		key = ""
-	}
-
 	names := append([]string{name}, c.Args().Tail()...)
 
 	if key != "" && s.Store.Exists(ctx, key) {
diff --git a/internal/backend/crypto/age/age.go b/internal/backend/crypto/age/age.go
index 7ff4afdf..f2dedfae 100644
--- a/internal/backend/crypto/age/age.go
+++ b/internal/backend/crypto/age/age.go
@@ -7,6 +7,7 @@ import (
 	"strings"
 	"time"
 
+	"filippo.io/age"
 	"github.com/blang/semver/v4"
 	"github.com/cenkalti/backoff/v4"
 	"github.com/gopasspw/gopass/internal/backend/crypto/age/agent"
@@ -87,7 +88,7 @@ func (a *Age) tryStartAgent(ctx context.Context) {
 
 	bo := backoff.NewExponentialBackOff()
 	bo.InitialInterval = 25 * time.Millisecond
-	bo.MaxElapsedTime = 5 * time.Second
+	bo.MaxElapsedTime = 3 * time.Second
 	op := func() error {
 		return client.Ping()
 	}
@@ -113,6 +114,13 @@ func (a *Age) tryStartAgent(ctx context.Context) {
 	if err := client.SendIdentities(strings.Join(idStrs, "\n")); err != nil {
 		debug.Log("failed to send identities to agent: %s", err)
 	}
+
+	// set timeout
+	if timeout := config.AsInt(config.String(ctx, "age.agent-timeout")); timeout > 0 {
+		if err := client.SetTimeout(timeout); err != nil {
+			debug.Log("failed to set agent timeout: %s", err)
+		}
+	}
 }
 
 // Initialized returns nil.
@@ -158,3 +166,12 @@ func (a *Age) GetFingerprint(ctx context.Context, key []byte) (string, error) {
 func (a *Age) Lock() {
 	a.askPass.Lock()
 }
+
+func (a *Age) identitiesToString(ids []age.Identity) (string, error) {
+	var sb strings.Builder
+	for _, id := range ids {
+		fmt.Fprintln(&sb, id)
+	}
+
+	return sb.String(), nil
+}
diff --git a/internal/backend/crypto/age/agent/agent.go b/internal/backend/crypto/age/agent/agent.go
index 3d2439e6..d345bc8c 100644
--- a/internal/backend/crypto/age/agent/agent.go
+++ b/internal/backend/crypto/age/agent/agent.go
@@ -12,9 +12,11 @@ import (
 	"os"
 	"os/signal"
 	"path/filepath"
+	"strconv"
 	"strings"
 	"sync"
 	"syscall"
+	"time"
 
 	"filippo.io/age"
 	"github.com/gopasspw/gopass/pkg/appdir"
@@ -32,6 +34,9 @@ type Agent struct {
 
 	mux        sync.Mutex
 	identities []age.Identity
+	locked     bool
+	timer      *time.Timer
+	timeout    time.Duration
 }
 
 // New creates a new agent.
@@ -45,6 +50,8 @@ func New() (*Agent, error) {
 
 	return &Agent{
 		socketPath: socketPath,
+		locked:     false,
+		timeout:    0,
 	}, nil
 }
 
@@ -97,6 +104,8 @@ func (a *Agent) Shutdown(ctx context.Context) {
 	if err := os.Remove(a.socketPath); err != nil {
 		debug.Log("failed to remove socket file: %s", err)
 	}
+
+	debug.Log("agent shut down")
 }
 
 func (a *Agent) handleConnection(ctx context.Context, conn net.Conn) {
@@ -120,6 +129,15 @@ func (a *Agent) handleConnection(ctx context.Context, conn net.Conn) {
 		switch cmd {
 		case "ping":
 			fmt.Fprintln(conn, "OK")
+		case "status":
+			a.mux.Lock()
+			locked := a.locked
+			a.mux.Unlock()
+			if locked {
+				fmt.Fprintln(conn, "OK locked")
+			} else {
+				fmt.Fprintln(conn, "OK")
+			}
 		case "identities":
 			if len(args) < 1 {
 				fmt.Fprintln(conn, "ERR missing identities")
@@ -151,7 +169,11 @@ func (a *Agent) handleConnection(ctx context.Context, conn net.Conn) {
 			}
 			plaintext, err := a.decrypt(ciphertext)
 			if err != nil {
-				fmt.Fprintln(conn, "ERR failed to decrypt: "+err.Error())
+				if err.Error() == "agent is locked" {
+					fmt.Fprintln(conn, "ERR agent is locked")
+				} else {
+					fmt.Fprintln(conn, "ERR failed to decrypt: "+err.Error())
+				}
 
 				continue
 			}
@@ -160,9 +182,31 @@ func (a *Agent) handleConnection(ctx context.Context, conn net.Conn) {
 			// clear all identities from memory
 			a.mux.Lock()
 			a.identities = nil
+			a.locked = true
+			a.mux.Unlock()
+
+			debug.Log("cleared identities from memory and locked agent")
+			fmt.Fprintln(conn, "OK")
+		case "unlock":
+			a.mux.Lock()
+			a.locked = false
 			a.mux.Unlock()
 
-			debug.Log("cleared identities from memory")
+			debug.Log("unlocked agent")
+			fmt.Fprintln(conn, "OK")
+		case "set-timeout":
+			if len(args) != 1 {
+				fmt.Fprintln(conn, "ERR missing timeout")
+
+				continue
+			}
+			timeout, err := strconv.Atoi(args[0])
+			if err != nil {
+				fmt.Fprintln(conn, "ERR failed to parse timeout: "+err.Error())
+
+				continue
+			}
+			a.setTimeout(time.Duration(timeout) * time.Second)
 			fmt.Fprintln(conn, "OK")
 		case "quit":
 			fmt.Fprintln(conn, "OK")
@@ -175,7 +219,42 @@ func (a *Agent) handleConnection(ctx context.Context, conn net.Conn) {
 	}
 }
 
+func (a *Agent) setTimeout(timeout time.Duration) {
+	a.mux.Lock()
+	defer a.mux.Unlock()
+
+	a.timeout = timeout
+	if a.timer != nil {
+		a.timer.Stop()
+	}
+	if a.timeout > 0 {
+		a.timer = time.AfterFunc(a.timeout, func() {
+			a.lock()
+		})
+	}
+}
+
+func (a *Agent) lock() {
+	a.mux.Lock()
+	defer a.mux.Unlock()
+
+	a.identities = nil
+	a.locked = true
+	if a.timer != nil {
+		a.timer.Stop()
+	}
+	debug.Log("cleared identities from memory and locked agent")
+}
+
 func (a *Agent) decrypt(ciphertext []byte) ([]byte, error) {
+	a.mux.Lock()
+	defer a.mux.Unlock()
+	if a.locked {
+		return nil, fmt.Errorf("agent is locked")
+	}
+	if a.timer != nil {
+		a.timer.Reset(a.timeout)
+	}
 	out := &bytes.Buffer{}
 	f := bytes.NewReader(ciphertext)
 	r, err := age.Decrypt(f, a.identities...)
diff --git a/internal/backend/crypto/age/agent/client.go b/internal/backend/crypto/age/agent/client.go
index 2db0292f..fffe1bb2 100644
--- a/internal/backend/crypto/age/agent/client.go
+++ b/internal/backend/crypto/age/agent/client.go
@@ -6,9 +6,11 @@ import (
 	"fmt"
 	"net"
 	"path/filepath"
+	"strconv"
 	"strings"
 
 	"github.com/gopasspw/gopass/pkg/appdir"
+	"github.com/gopasspw/gopass/pkg/debug"
 )
 
 // Client is a client for the age agent.
@@ -27,11 +29,14 @@ func (c *Client) connect() (net.Conn, error) {
 	if err := c.checkSocketSecurity(); err != nil {
 		return nil, err
 	}
+
+	debug.Log("connecting to agent at %s", c.socketPath)
 	conn, err := net.Dial("unix", c.socketPath)
 	if err != nil {
 		return nil, fmt.Errorf("failed to connect to agent: %w", err)
 	}
 
+	debug.Log("connected to agent at %s", c.socketPath)
 	return conn, nil
 }
 
@@ -68,6 +73,11 @@ func (c *Client) Ping() error {
 	return err
 }
 
+// Status returns the agent's status.
+func (c *Client) Status() (string, error) {
+	return c.send("status")
+}
+
 // SendIdentities sends the identities to the agent.
 func (c *Client) SendIdentities(ids string) error {
 	_, err := c.send("identities " + ids)
@@ -99,6 +109,20 @@ func (c *Client) Lock() error {
 	return err
 }
 
+// Unlock unlocks the agent.
+func (c *Client) Unlock() error {
+	_, err := c.send("unlock")
+
+	return err
+}
+
+// SetTimeout sets the agent's timeout.
+func (c *Client) SetTimeout(timeout int) error {
+	_, err := c.send("set-timeout " + strconv.Itoa(timeout))
+
+	return err
+}
+
 // Quit quits the agent.
 func (c *Client) Quit() error {
 	_, err := c.send("quit")
diff --git a/internal/backend/crypto/age/agent_starter_unix.go b/internal/backend/crypto/age/agent_starter_unix.go
index bec9b64f..16e51f09 100644
--- a/internal/backend/crypto/age/agent_starter_unix.go
+++ b/internal/backend/crypto/age/agent_starter_unix.go
@@ -10,7 +10,9 @@ import (
 )
 
 func startAgent(_ context.Context) error {
-	cmd := exec.Command(os.Args[0], "age", "agent")
+	cmd := exec.Command(os.Args[0], "age", "agent", "start")
+	cmd.Env = os.Environ()
+	cmd.Stderr = os.Stderr
 	cmd.SysProcAttr = &syscall.SysProcAttr{
 		Setpgid: true,
 	}
diff --git a/internal/backend/crypto/age/agent_starter_windows.go b/internal/backend/crypto/age/agent_starter_windows.go
index 9205b189..aef044ff 100644
--- a/internal/backend/crypto/age/agent_starter_windows.go
+++ b/internal/backend/crypto/age/agent_starter_windows.go
@@ -8,7 +8,9 @@ import (
 	"os/exec"
 )
 
-func startAgent(ctx context.Context) error {
-	cmd := exec.Command(os.Args[0], "age", "agent")
+func startAgent(_ context.Context) error {
+	cmd := exec.Command(os.Args[0], "age", "agent", "start")
+	cmd.Env = os.Environ()
+	cmd.Stderr = os.Stderr
 	return cmd.Start()
 }
diff --git a/internal/backend/crypto/age/commands.go b/internal/backend/crypto/age/commands.go
index 68cb1e9d..81ba37a1 100644
--- a/internal/backend/crypto/age/commands.go
+++ b/internal/backend/crypto/age/commands.go
@@ -40,16 +40,27 @@ func (l loader) Commands() []*cli.Command {
 			Subcommands: []*cli.Command{
 				{
 					Name:  "agent",
-					Usage: "Start the age agent",
-					Description: "Start the age agent, this will start a background process that will cache your age identities in memory and provide them to gopass on demand. " +
+					Usage: "Manage the age agent",
+					Description: "Manage the age agent, this will start a background process that will cache your age identities in memory and provide them to gopass on demand. " +
 						"This is optional, but recommended if you use age identities that require a password or are managed by a plugin.",
-					Action: l.agent,
+					Action: func(c *cli.Context) error {
+						if err := cli.ShowSubcommandHelp(c); err != nil {
+							return exit.Error(exit.Unknown, err, "failed to show help")
+						}
+
+						return exit.Error(exit.Usage, nil, "Please specify a subcommand")
+					},
 					Subcommands: []*cli.Command{
 						{
-							Name: "stop",
-							Usage: "Stop the age agent, this will remove all cached identities and stop the agent. " +
-								"Any running gopass instance using the agent will no longer be able to decrypt secrets.",
-							Description: "Stop the age agent, this will remove all cached identities and stop the agent.",
+							Name:        "start",
+							Usage:       "Start the age agent",
+							Description: "Start the age agent",
+							Action:      l.agent,
+						},
+						{
+							Name:        "stop",
+							Usage:       "Stop the age agent",
+							Description: "Stop the age agent",
 							Action: func(c *cli.Context) error {
 								ctx := ctxutil.WithGlobalFlags(c)
 								client := agent.NewClient()
@@ -68,16 +79,40 @@ func (l loader) Commands() []*cli.Command {
 							Action: func(c *cli.Context) error {
 								ctx := ctxutil.WithGlobalFlags(c)
 								client := agent.NewClient()
-								if err := client.Ping(); err != nil {
+								status, err := client.Status()
+								if err != nil {
 									out.Printf(ctx, "Age agent is not running")
 
 									return exit.Error(exit.Unknown, err, "agent not running")
 								}
 								out.Printf(ctx, "Age agent is running")
+								if status == "locked" {
+									out.Printf(ctx, " (locked)")
+								}
+
+								return nil
+							},
+						},
+						{
+							Name:        "unlock",
+							Usage:       "Unlock the age agent",
+							Description: "Unlock the age agent, this will allow gopass to ask for your password again when decrypting",
+							Action: func(c *cli.Context) error {
+								client := agent.NewClient()
+								if err := client.Unlock(); err != nil {
+									return exit.Error(exit.Unknown, err, "failed to unlock agent: %s", err)
+								}
+								out.Printf(c.Context, "Age agent unlocked")
 
 								return nil
 							},
 						},
+						{
+							Name:        "lock",
+							Usage:       "Lock the age agent",
+							Description: "Lock the age agent",
+							Action:      l.lock,
+						},
 					},
 				},
 				{
@@ -279,6 +314,7 @@ func (l loader) Commands() []*cli.Command {
 					Usage:       "Lock the age agent",
 					Description: "Lock the age agent, this will remove all cached identities from memory and require you to re-enter any passwords for your identities when decrypting",
 					Action:      l.lock,
+					Hidden:      true,
 				},
 			},
 		},
diff --git a/internal/backend/crypto/age/decrypt.go b/internal/backend/crypto/age/decrypt.go
index e32700f0..949e983f 100644
--- a/internal/backend/crypto/age/decrypt.go
+++ b/internal/backend/crypto/age/decrypt.go
@@ -6,9 +6,11 @@ import (
 	"fmt"
 	"io"
 	"os"
+	"strings"
 
 	"filippo.io/age"
 	"github.com/gopasspw/gopass/internal/backend/crypto/age/agent"
+
 	"github.com/gopasspw/gopass/internal/config"
 	"github.com/gopasspw/gopass/pkg/ctxutil"
 	"github.com/gopasspw/gopass/pkg/debug"
@@ -17,12 +19,12 @@ import (
 // Decrypt will attempt to decrypt the given payload.
 func (a *Age) Decrypt(ctx context.Context, ciphertext []byte) ([]byte, error) {
 	if config.Bool(ctx, "age.agent-enabled") {
-		client := agent.NewClient()
-		plaintext, err := client.Decrypt(ciphertext)
+		plaintext, err := a.decryptWithAgent(ctx, ciphertext)
 		if err == nil {
 			return plaintext, nil
 		}
 		debug.Log("failed to decrypt with agent: %s", err)
+		debug.Log("falling back to direct decryption")
 	}
 
 	if !ctxutil.HasPasswordCallback(ctx) {
@@ -43,6 +45,47 @@ func (a *Age) Decrypt(ctx context.Context, ciphertext []byte) ([]byte, error) {
 	return a.decrypt(ciphertext, ids...)
 }
 
+func (a *Age) decryptWithAgent(ctx context.Context, ciphertext []byte) ([]byte, error) {
+	client := agent.NewClient()
+	plaintext, err := client.Decrypt(ciphertext)
+	if err == nil {
+		return plaintext, nil
+	}
+
+	if !strings.Contains(err.Error(), "agent is locked") {
+		debug.Log("failed to decrypt with agent: %s", err)
+
+		return nil, err
+	}
+
+	debug.Log("agent is locked, trying to unlock")
+	// unlock the agent
+	if err := client.Unlock(); err != nil {
+		debug.Log("failed to unlock agent: %s", err)
+	}
+	// get identities
+	ids, err := a.getAllIds(ctx)
+	if err != nil {
+		return nil, err
+	}
+	// send identities to agent
+	sIds, err := a.identitiesToString(ids)
+	if err != nil {
+		return nil, err
+	}
+	if err := client.SendIdentities(sIds); err != nil {
+		debug.Log("failed to send identities to agent: %s", err)
+	}
+	// set timeout
+	if timeout := config.AsInt(config.String(ctx, "age.agent-timeout")); timeout > 0 {
+		if err := client.SetTimeout(timeout); err != nil {
+			debug.Log("failed to set agent timeout: %s", err)
+		}
+	}
+	// retry decryption
+	return client.Decrypt(ciphertext)
+}
+
 func (a *Age) decrypt(ciphertext []byte, ids ...age.Identity) ([]byte, error) {
 	debug.Log("decrypting with %d ids", len(ids))
 
diff --git a/internal/config/config.go b/internal/config/config.go
index 51b4bef8..b3f20e5c 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -48,6 +48,7 @@ func newGitconfig() *gitconfig.Configs {
 
 var defaults = map[string]string{
 	"age.agent-enabled":      "false",
+	"age.agent-timeout":      "0",
 	"core.autopush":          "true",
 	"core.autosync":          "true",
 	"core.cliptimeout":       "45",
diff --git a/internal/store/root/move.go b/internal/store/root/move.go
index 34f974ad..3532327a 100644
--- a/internal/store/root/move.go
+++ b/internal/store/root/move.go
@@ -46,6 +46,10 @@ func (r *Store) move(ctx context.Context, from, to string, del bool) error {
 		return err
 	}
 
+	if !ctxutil.IsGitCommit(ctx) {
+		return nil
+	}
+
 	commitMsg := ctxutil.GetCommitMessage(ctx)
 	if err := subFrom.Storage().TryCommit(ctx, commitMsg); del && err != nil {
 		return fmt.Errorf("failed to commit changes to git (%s): %w", subFrom.Alias(), err)
diff --git a/main.go b/main.go
index 93fd8418..e9e63439 100644
--- a/main.go
+++ b/main.go
@@ -87,6 +87,7 @@ func main() {
 	ctx = queue.WithQueue(ctx, q)
 	ctx, app := setupApp(ctx, sv)
 
+	debug.Log("gopass %s starting ...", sv.String())
 	if err := app.RunContext(ctx, os.Args); err != nil {
 		log.Fatal(err)
 	}
@@ -95,6 +96,8 @@ func main() {
 	_ = q.Close(ctx)
 
 	writeMemProfile()
+
+	debug.Log("gopass %s shutting down ...\n\n", sv.String())
 }
 
 //nolint:wrapcheck



Test patch:
diff --git a/internal/action/config_test.go b/internal/action/config_test.go
index ab71ea1e..fabc136a 100644
--- a/internal/action/config_test.go
+++ b/internal/action/config_test.go
@@ -38,6 +38,7 @@ func TestConfig(t *testing.T) {
 		c := gptest.CliCtx(ctx, t)
 		require.NoError(t, act.Config(c))
 		want := `age.agent-enabled = false
+age.agent-timeout = 0
 core.autoimport = true
 core.autopush = true
 core.autosync = true
@@ -82,6 +83,7 @@ generate.autoclip = true
 
 		act.printConfigValues(ctx, "")
 		want := `age.agent-enabled = false
+age.agent-timeout = 0
 core.autoimport = true
 core.autopush = true
 core.autosync = true
@@ -119,6 +121,7 @@ generate.autoclip = true
 
 		act.ConfigComplete(gptest.CliCtx(ctx, t))
 		want := `age.agent-enabled
+age.agent-timeout
 core.autoimport
 core.autopush
 core.autosync
diff --git a/internal/backend/crypto/age/agent/agent_test.go b/internal/backend/crypto/age/agent/agent_test.go
index 739ccadf..03741521 100644
--- a/internal/backend/crypto/age/agent/agent_test.go
+++ b/internal/backend/crypto/age/agent/agent_test.go
@@ -57,3 +57,58 @@ func TestAgent(t *testing.T) {
 	// test quit
 	require.NoError(t, c.Quit())
 }
+
+func TestAgentAutoLock(t *testing.T) {
+	ctx := t.Context()
+	ctx = termio.WithPassPromptFunc(ctx, func(ctx context.Context, prompt string) (string, error) {
+		return "test", nil
+	})
+
+	// start agent
+	a, err := New()
+	require.NoError(t, err)
+
+	go func() {
+		_ = a.Run(ctx)
+	}()
+	defer a.Shutdown(ctx)
+
+	// wait for it to be ready
+	time.Sleep(time.Second)
+
+	// create client
+	c := NewClient()
+	require.NoError(t, c.Ping())
+
+	// set timeout
+	require.NoError(t, c.SetTimeout(1))
+
+	// test decrypt
+	id, err := age.GenerateX25519Identity()
+	require.NoError(t, err)
+
+	plaintext := []byte("hello world")
+	buf := &bytes.Buffer{}
+	wc, err := age.Encrypt(buf, id.Recipient())
+	require.NoError(t, err)
+	_, _ = wc.Write(plaintext)
+	require.NoError(t, wc.Close())
+	ciphertext := buf.Bytes()
+
+	require.NoError(t, c.SendIdentities(id.String()))
+
+	decrypted, err := c.Decrypt(ciphertext)
+	require.NoError(t, err)
+	require.Equal(t, plaintext, decrypted)
+
+	// wait for auto-lock
+	time.Sleep(2 * time.Second)
+
+	// check if locked
+	_, err = c.Decrypt(ciphertext)
+	require.Error(t, err)
+	require.Contains(t, err.Error(), "agent is locked")
+
+	// test quit
+	require.NoError(t, c.Quit())
+}
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index 9ff95e06..06a78eda 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -33,6 +33,7 @@ func TestConfig(t *testing.T) {
 	// test default values
 	assert.Equal(t, []string{
 		"age.agent-enabled",
+		"age.agent-timeout",
 		"core.autopush",
 		"core.autosync",
 		"core.bool",
diff --git a/tests/config_test.go b/tests/config_test.go
index d867b215..32042a98 100644
--- a/tests/config_test.go
+++ b/tests/config_test.go
@@ -16,6 +16,7 @@ func TestBaseConfig(t *testing.T) {
 	require.NoError(t, err)
 
 	wanted := `age.agent-enabled = false
+age.agent-timeout = 0
 core.autopush = true
 core.autosync = true
 core.cliptimeout = 45
@@ -70,6 +71,7 @@ func TestMountConfig(t *testing.T) {
 	require.NoError(t, err)
 
 	wanted := `age.agent-enabled = false
+age.agent-timeout = 0
 core.autopush = true
 core.autosync = true
 core.cliptimeout = 45