diff --git a/async/examples/sshclient.rs b/async/examples/sshclient.rs
index c6c65e973d61dc2ae7362d68132da97c04ad2212..295f4d875bd9d05e94b60d31c61026d3bd6d8faf 100644
--- a/async/examples/sshclient.rs
+++ b/async/examples/sshclient.rs
@@ -3,7 +3,7 @@ use {
     // crate::error::Error,
     log::{debug, error, info, log, trace, warn},
 };
-use anyhow::{Context, Result, anyhow};
+use anyhow::{Context, Result, anyhow, bail};
 use embassy_sync::{mutex::Mutex, blocking_mutex::raw::NoopRawMutex};
 
 use tokio::net::TcpStream;
@@ -58,6 +58,10 @@ struct Args {
     /// force no pty
     force_no_pty: bool,
 
+    #[argh(option, short='s')]
+    /// ssh subsystem (eg "sftp")
+    subsystem: Option<String>,
+
     #[argh(positional)]
     /// command
     cmd: Vec<String>,
@@ -117,8 +121,8 @@ fn setup_log(args: &Args) -> Result<()> {
     .add_filter_allow_str("sshclient")
     // not debugging these bits of the stack at present
     // .add_filter_ignore_str("sunset::traffic")
-    .add_filter_ignore_str("sunset::runner")
-    .add_filter_ignore_str("sunset_embassy")
+    // .add_filter_ignore_str("sunset::runner")
+    // .add_filter_ignore_str("sunset_embassy")
     .set_time_offset_to_local().expect("Couldn't get local timezone")
     .build();
 
@@ -181,13 +185,25 @@ async fn run(args: Args) -> Result<()> {
     trace!("tracing main");
     debug!("verbose main");
 
-    let (cmd, wantpty) = if args.cmd.is_empty() {
-        (None, true)
+    if !args.cmd.is_empty() && args.subsystem.is_some() {
+        bail!("can't have '-s subsystem' with a command")
+    }
+
+    let mut want_pty = true;
+    let cmd = if args.cmd.is_empty() {
+        None
     } else {
-        (Some(args.cmd.join(" ")), false)
+        want_pty = false;
+        Some(args.cmd.join(" "))
     };
 
-    let wantpty = wantpty && !args.force_no_pty;
+    if args.subsystem.is_some() {
+        want_pty = false;
+    }
+
+    if args.force_no_pty {
+        want_pty = false
+    }
 
     let ssh_task = spawn_local(async move {
         let mut rxbuf = Zeroizing::new(vec![0; 3000]);
@@ -197,17 +213,26 @@ async fn run(args: Args) -> Result<()> {
         let mut app = CmdlineClient::new(
             args.username.as_ref().unwrap(),
             &args.host,
-            args.port,
-            cmd,
-            wantpty,
-            );
+        );
+
+        app.port(args.port);
+
+        if want_pty {
+            app.pty();
+        }
+        if let Some(c) = cmd {
+            app.exec(&c);
+        }
+        if let Some(c) = args.subsystem {
+            app.subsystem(&c);
+        }
         for i in &args.identityfile {
             app.add_authkey(read_key(&i).with_context(|| format!("loading key {i}"))?);
         }
 
         let agent = load_agent_keys(&mut app).await;
         if let Some(agent) = agent {
-            app.set_agent(agent)
+            app.agent(agent);
         }
 
         // Connect to a peer
diff --git a/async/src/cmdline_client.rs b/async/src/cmdline_client.rs
index 198d1a3aecbff61594b4afd5cd2bd9deb747970b..ecc54d9ce0b928c8569fd9e42dda62cae038df84 100644
--- a/async/src/cmdline_client.rs
+++ b/async/src/cmdline_client.rs
@@ -5,9 +5,9 @@ use log::{debug, error, info, log, trace, warn};
 use core::str::FromStr;
 use core::fmt::Debug;
 
-use sunset::{AuthSigMsg, SignKey, OwnedSig};
+use sunset::{AuthSigMsg, SignKey, OwnedSig, Pty, sshnames};
 use sunset::{BhError, BhResult};
-use sunset::{ChanMsg, ChanMsgDetails, Error, Result, Runner};
+use sunset::{Error, Result, Runner, SessionCommand};
 use sunset::behaviour::{UnusedCli, UnusedServ};
 use sunset_embassy::*;
 
@@ -32,6 +32,10 @@ use crate::pty::win_size;
 enum CmdlineState<'a> {
     PreAuth,
     Authed,
+    Opening {
+        io: ChanInOut<'a, CmdlineHooks<'a>, UnusedServ>,
+        extin: Option<ChanIn<'a, CmdlineHooks<'a>, UnusedServ>>,
+    },
     Ready {
         io: ChanInOut<'a, CmdlineHooks<'a>, UnusedServ>,
         extin: Option<ChanIn<'a, CmdlineHooks<'a>, UnusedServ>>,
@@ -40,12 +44,13 @@ enum CmdlineState<'a> {
 
 enum Msg {
     Authed,
+    Opened,
     /// The SSH session exited
     Exited,
 }
 
 pub struct CmdlineClient {
-    cmd: Option<String>,
+    cmd: SessionCommand<String>,
     want_pty: bool,
 
     // to be passed to hooks
@@ -56,13 +61,12 @@ pub struct CmdlineClient {
     agent: Option<AgentClient>,
 
     notify: Channel<SunsetRawMutex, Msg, 1>,
+    pty_guard: Option<RawPtyGuard>,
 }
 
 pub struct CmdlineRunner<'a> {
     state: CmdlineState<'a>,
-    pty_guard: Option<RawPtyGuard>,
 
-    cmd: &'a Option<String>,
     want_pty: bool,
 
     notify: Receiver<'a, SunsetRawMutex, Msg, 1>,
@@ -74,22 +78,109 @@ pub struct CmdlineHooks<'a> {
     host: &'a str,
     port: u16,
     agent: Option<AgentClient>,
+    cmd: &'a SessionCommand<String>,
+    pty: Option<Pty>,
 
     notify: Sender<'a, SunsetRawMutex, Msg, 1>,
 }
 
-impl<'a> Debug for CmdlineHooks<'a> {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.write_str("CmdlineHooks")
+impl CmdlineClient {
+    pub fn new(username: impl AsRef<str>, host: impl AsRef<str>) -> Self {
+        Self {
+            cmd: SessionCommand::Shell,
+            want_pty: false,
+            agent: None,
+
+            notify: Channel::new(),
+            pty_guard: None,
+
+            username: username.as_ref().into(),
+            host: host.as_ref().into(),
+            port: sshnames::SSH_PORT,
+            authkeys: Default::default(),
+        }
     }
+
+    pub fn split(&mut self) -> (CmdlineHooks, CmdlineRunner) {
+
+        let pty = self.make_pty();
+
+        let authkeys = core::mem::replace(&mut self.authkeys, Default::default());
+
+        let runner = CmdlineRunner::new(pty.is_some(), self.notify.receiver());
+
+        let hooks = CmdlineHooks {
+            username: &self.username,
+            host: &self.host,
+            port: self.port,
+            authkeys,
+            agent: self.agent.take(),
+            cmd: &self.cmd,
+            pty,
+            notify: self.notify.sender(),
+        };
+
+        (hooks, runner)
+    }
+
+    pub fn port(&mut self, port: u16) -> &mut Self {
+        self.port = port;
+        self
+    }
+
+    pub fn pty(&mut self) -> &mut Self {
+        self.want_pty = true;
+        self
+    }
+
+    pub fn exec(&mut self, cmd: &str) -> &mut Self {
+        self.cmd = SessionCommand::Exec(cmd.into());
+        self
+    }
+
+    pub fn subsystem(&mut self, subsystem: &str) -> &mut Self {
+        self.cmd = SessionCommand::Subsystem(subsystem.into());
+        self
+    }
+
+    pub fn add_authkey(&mut self, k: SignKey) {
+        self.authkeys.push_back(k)
+    }
+
+    pub fn agent(&mut self, agent: AgentClient) {
+        self.agent = Some(agent)
+    }
+
+    fn make_pty(&mut self) -> Option<Pty> {
+        let mut pty = None;
+        if self.want_pty {
+            match pty::current_pty() {
+                Ok(p) => pty = Some(p),
+                Err(e) => warn!("Failed getting current pty: {e:?}"),
+            }
+
+            if pty.is_some() {
+                // switch to raw pty mode
+                match raw_pty() {
+                    Ok(p) => self.pty_guard = Some(p),
+                    Err(e) => {
+                        warn!("Failed getting raw pty: {e:?}");
+                        pty = None
+                    }
+                }
+
+            }
+        }
+        pty
+    }
+
 }
 
+
 impl<'a> CmdlineRunner<'a> {
-    fn new(cmd: &'a Option<String>, want_pty: bool, notify: Receiver<'a, SunsetRawMutex, Msg, 1>) -> Self {
+    fn new(want_pty: bool, notify: Receiver<'a, SunsetRawMutex, Msg, 1>) -> Self {
         Self {
             state: CmdlineState::PreAuth,
-            pty_guard: None,
-            cmd,
             want_pty,
             notify,
         }
@@ -209,8 +300,12 @@ impl<'a> CmdlineRunner<'a> {
                             self.state = CmdlineState::Authed;
                             debug!("Opening a new session channel");
                             self.open_session(cli).await?;
-                            if let CmdlineState::Ready { io, extin } = &self.state {
-                                chanio.set(Self::chan_run(io.clone(), extin.clone()).fuse())
+                        }
+                        Msg::Opened => {
+                            let st = core::mem::replace(&mut self.state, CmdlineState::Authed);
+                            if let CmdlineState::Opening { io, extin } = st {
+                                chanio.set(Self::chan_run(io.clone(), extin.clone()).fuse());
+                                self.state = CmdlineState::Ready { io, extin };
                             }
                         }
                         Msg::Exited => {
@@ -241,23 +336,20 @@ impl<'a> CmdlineRunner<'a> {
     async fn open_session(&mut self, cli: &'a SSHClient<'a, CmdlineHooks<'a>>) -> Result<()> {
         debug_assert!(matches!(self.state, CmdlineState::Authed));
 
-        let cmd = self.cmd.as_ref().map(|s| s.as_str());
         let (io, extin) = if self.want_pty {
-            // TODO expect
-            let pty = pty::current_pty().expect("pty fetch");
-            self.pty_guard = Some(raw_pty().expect("raw pty"));
-            let io = cli.open_session_pty(cmd, pty).await?;
+            let io = cli.open_session_pty().await?;
             (io, None)
         } else {
-            let (io, extin) = cli.open_session_nopty(cmd).await?;
+            let (io, extin) = cli.open_session_nopty().await?;
             (io, Some(extin))
         };
-        self.state = CmdlineState::Ready { io, extin };
+        self.state = CmdlineState::Opening { io, extin };
         Ok(())
     }
 
     async fn window_change_signal(&mut self) {
         let io = match &self.state {
+            CmdlineState::Opening { io, ..} => io,
             CmdlineState::Ready { io, ..} => io,
             _ => return,
         };
@@ -276,54 +368,7 @@ impl<'a> CmdlineRunner<'a> {
     }
 }
 
-impl CmdlineClient {
-    pub fn new(username: impl AsRef<str>, host: impl AsRef<str>, port: u16,
-        cmd: Option<impl AsRef<str>>, want_pty: bool, ) -> Self {
-        Self {
-
-            // TODO: shorthand for this?
-            cmd: cmd.map(|c| c.as_ref().into()),
-            want_pty,
-            agent: None,
-
-            notify: Channel::new(),
-
-            username: username.as_ref().into(),
-            host: host.as_ref().into(),
-            port,
-            authkeys: Default::default(),
-        }
-    }
-
-    pub fn set_agent(&mut self, agent: AgentClient) {
-        self.agent = Some(agent)
-    }
-
-    pub fn split(&mut self) -> (CmdlineHooks, CmdlineRunner) {
-        let ak = core::mem::replace(&mut self.authkeys, Default::default());
-        let hooks = CmdlineHooks::new(&self.username, &self.host, self.port, ak, self.agent.take(), self.notify.sender());
-        let runner = CmdlineRunner::new(&self.cmd, self.want_pty, self.notify.receiver());
-        (hooks, runner)
-    }
-
-    pub fn add_authkey(&mut self, k: SignKey) {
-        self.authkeys.push_back(k)
-    }
-}
-
 impl<'a> CmdlineHooks<'a> {
-    fn new(username: &'a str, host: &'a str, port: u16, authkeys: VecDeque<SignKey>,
-        agent: Option<AgentClient>, notify: Sender<'a, SunsetRawMutex, Msg, 1>) -> Self {
-        Self {
-            authkeys,
-            username,
-            host,
-            port,
-            agent,
-            notify,
-        }
-    }
-
     /// Notify the `CmdlineClient` that the main SSH session has exited.
     ///
     /// This will cause the `CmdlineRunner` to finish flushing output and terminate.
@@ -389,4 +434,20 @@ impl sunset::CliBehaviour for CmdlineHooks<'_> {
             warn!("Full notification queue");
         }
     }
+
+    async fn session_opened(&mut self, chan: sunset::ChanNum, opener: &mut sunset::SessionOpener<'_, '_, '_>) -> BhResult<()> {
+        if let Some(p) = self.pty.take() {
+            opener.pty(p)
+        }
+        opener.cmd(self.cmd);
+        self.notify.send(Msg::Opened).await;
+        Ok(())
+    }
+}
+
+impl<'a> Debug for CmdlineHooks<'a> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str("CmdlineHooks")
+    }
 }
+
diff --git a/embassy/src/client.rs b/embassy/src/client.rs
index 64a1cdee260174ae3bc2fbaa31ee562f4978cffe..0369a124acf2520a6a2cc9e09dc324e81c341dc9 100644
--- a/embassy/src/client.rs
+++ b/embassy/src/client.rs
@@ -38,10 +38,10 @@ impl<'a, C: CliBehaviour> SSHClient<'a, C> {
         self.sunset.exit().await
     }
 
-    pub async fn open_session_nopty(&'a self, exec: Option<&str>)
+    pub async fn open_session_nopty(&'a self)
     -> Result<(ChanInOut<'a, C, S>, ChanIn<'a, C, S>)> {
         let chan = self.sunset.with_runner(|runner| {
-            runner.open_client_session(exec, None)
+            runner.open_client_session()
         }).await?;
 
         let num = chan.num();
@@ -52,11 +52,9 @@ impl<'a, C: CliBehaviour> SSHClient<'a, C> {
         Ok((cstd, cerr))
     }
 
-    pub async fn open_session_pty(&'a self, exec: Option<&str>, pty: Pty)
-    -> Result<ChanInOut<'a, C, S>> {
-
+    pub async fn open_session_pty(&'a self) -> Result<ChanInOut<'a, C, S>> {
         let chan = self.sunset.with_runner(|runner| {
-            runner.open_client_session(exec, Some(pty))
+            runner.open_client_session()
         }).await?;
 
         let num = chan.num();
diff --git a/src/behaviour.rs b/src/behaviour.rs
index 996705d7678780f5677f35e76a47ff3d0aec50b1..3e17703f50400c2cad71f7e4d9effc5fc76bc6cc 100644
--- a/src/behaviour.rs
+++ b/src/behaviour.rs
@@ -176,10 +176,15 @@ pub trait CliBehaviour {
     // functions. `Events` may be handled asynchronously so wouldn't
     // guarantee that.
     #[allow(unused)]
-    fn show_banner(&self, banner: TextString, language: TextString) {
+    fn show_banner(&mut self, banner: TextString, language: TextString) {
     }
     // TODO: postauth channel callbacks
 
+    #[allow(unused)]
+    async fn session_opened(&mut self, chan: ChanNum, opener: &mut SessionOpener<'_, '_, '_>) -> BhResult<()> {
+        Err(BhError::Fail)
+    }
+
     #[allow(unused)]
     fn open_tcp_forwarded(&mut self, chan: ChanHandle, t: &ForwardedTcpip) -> ChanOpened {
         ChanOpened::Failure((ChanFail::SSH_OPEN_UNKNOWN_CHANNEL_TYPE, chan))
diff --git a/src/channel.rs b/src/channel.rs
index cadeb6174c755f3d806d56a1dc5c6719759cf608..ac05b25ddf3cc24f5677e17b3defdb6709f18769 100644
--- a/src/channel.rs
+++ b/src/channel.rs
@@ -33,11 +33,10 @@ impl<C: CliBehaviour, S: ServBehaviour> Channels<C, S> {
     pub fn open<'b>(
         &mut self,
         ty: packets::ChannelOpenType<'b>,
-        init_req: InitReqs,
     ) -> Result<(ChanNum, Packet<'b>)> {
         let num = self.unused_chan()?;
 
-        let chan = Channel::new(num, (&ty).into(), init_req);
+        let chan = Channel::new(num, (&ty).into());
         let p = packets::ChannelOpen {
             num: num.0,
             initial_window: chan.recv.window as u32,
@@ -135,7 +134,7 @@ impl<C: CliBehaviour, S: ServBehaviour> Channels<C, S> {
     /// Creates a new channel in InOpen state.
     fn reserve_chan(&mut self, co: &ChannelOpen<'_>) -> Result<&mut Channel> {
         let num = self.unused_chan()?;
-        let mut chan = Channel::new(num, (&co.ty).into(), Vec::new());
+        let mut chan = Channel::new(num, (&co.ty).into());
         chan.send = Some(ChanDir {
             num: co.num,
             max_packet: co.max_packet as usize,
@@ -207,7 +206,7 @@ impl<C: CliBehaviour, S: ServBehaviour> Channels<C, S> {
         s: &mut TrafSend) -> Result<()> {
         let ch = self.get(num)?;
         match ch.ty {
-            ChanType::Session => ch.request(ReqDetails::WinChange(winch), s),
+            ChanType::Session => Req::WinChange(winch).send(ch, s),
             _ => error::BadChannelData.fail(),
         }
     }
@@ -313,21 +312,24 @@ impl<C: CliBehaviour, S: ServBehaviour> Channels<C, S> {
             Packet::ChannelOpenConfirmation(p) => {
                 let ch = self.get_any_mut(ChanNum(p.num))?;
                 match ch.state {
-                    ChanState::Opening { .. } => {
-                        let init_state =
-                            mem::replace(&mut ch.state, ChanState::Normal);
-                        if let ChanState::Opening { init_req } = init_state {
-                            debug_assert!(ch.send.is_none());
-                            ch.send = Some(ChanDir {
-                                num: p.sender_num,
-                                max_packet: p.max_packet as usize,
-                                window: p.initial_window as usize,
-                            });
-                            for r in init_req {
-                                ch.request(r, s)?
+                    ChanState::Opening => {
+                        debug_assert!(ch.send.is_none());
+                        ch.send = Some(ChanDir {
+                            num: p.sender_num,
+                            max_packet: p.max_packet as usize,
+                            window: p.initial_window as usize,
+                        });
+
+                        if matches!(ch.ty, ChanType::Session) {
+                            // let the CliBehaviour open a shell etc
+                            let mut opener = SessionOpener::new(&ch, s);
+                            let r = b.client()?.session_opened(ch.num(), &mut opener).await;
+                            if let Err(e) = r {
+                                trace!("Error from session_opened");
                             }
-                            ch.state = ChanState::Normal;
                         }
+
+                        ch.state = ChanState::Normal;
                     }
                     _ => {
                         trace!("Bad channel state");
@@ -395,7 +397,7 @@ impl<C: CliBehaviour, S: ServBehaviour> Channels<C, S> {
                 trace!("channel success, TODO");
             }
             Packet::ChannelFailure(_p) => {
-                todo!("ChannelFailure");
+                trace!("channel failure, TODO");
             }
             _ => Error::bug_msg("unreachable")?,
         };
@@ -453,13 +455,11 @@ pub struct ModePair {
 
 #[derive(Debug)]
 pub struct Pty {
-    // or could we put String into packets::PtyReq and serialize modes there...
     pub term: String<MAX_TERM>,
     pub cols: u32,
     pub rows: u32,
     pub width: u32,
     pub height: u32,
-    // TODO: perhaps we need something serializable here
     pub modes: Vec<ModePair, { termmodes::NUM_MODES }>,
 }
 
@@ -483,42 +483,29 @@ pub(crate) type ExecString = heapless::String<MAX_EXEC>;
 /// Like a `packets::ChannelReqType` but with storage.
 /// Lifetime-free variants have the packet part directly.
 #[derive(Debug)]
-pub enum ReqDetails {
+pub enum Req<'a> {
     // TODO let hook impls provide a string type?
     Shell,
-    Exec(ExecString),
+    Exec(&'a str),
+    Subsystem(&'a str),
     Pty(Pty),
-    // Subsytem { subsystem: heapless::String<MAX_EXEC> },
     WinChange(packets::WinChange),
     Break(packets::Break),
+    // Signal,
+    // ExitStatus,
+    // ExitSignal,
 }
 
-#[derive(Debug)]
-pub struct Req {
-    // recipient's channel number
-    num: u32,
-    details: ReqDetails,
-}
-
-impl ReqDetails {
-    fn want_reply(&self) -> bool {
-        match self {
-            Self::WinChange(_) => false,
-            _ => true,
-        }
-    }
-}
-
-impl Req {
-    pub(crate) fn packet<'a>(&'a self) -> Result<Packet<'a>> {
-        let num = self.num;
-        let want_reply = self.details.want_reply();
-        let ty = match &self.details {
-            ReqDetails::Shell => ChannelReqType::Shell,
-            ReqDetails::Pty(pty) => {
+impl Req<'_> {
+    pub(crate) fn send(self, ch: &Channel, s: &mut TrafSend) -> Result<()> {
+        let t;
+        let req = match self {
+            Req::Shell => ChannelReqType::Shell,
+            Req::Pty(pty) => {
                 debug!("TODO implement pty modes");
+                t = pty.term;
                 ChannelReqType::Pty(packets::PtyReq {
-                    term: TextString(pty.term.as_bytes()),
+                    term: TextString(t.as_bytes()),
                     cols: pty.cols,
                     rows: pty.rows,
                     width: pty.width,
@@ -526,17 +513,45 @@ impl Req {
                     modes: BinString(&[]),
                 })
             }
-            ReqDetails::Exec(cmd) => {
-                ChannelReqType::Exec(packets::Exec { command: cmd.as_str().into() })
+            Req::Exec(cmd) => {
+                ChannelReqType::Exec(packets::Exec { command: cmd.into() })
             }
-            ReqDetails::WinChange(rt) => ChannelReqType::WinChange(rt.clone()),
-            ReqDetails::Break(rt) => ChannelReqType::Break(rt.clone()),
+            Req::Subsystem(cmd) => {
+                ChannelReqType::Subsystem(packets::Subsystem { subsystem: cmd.into() })
+            }
+            Req::WinChange(rt) => ChannelReqType::WinChange(rt),
+            Req::Break(rt) => ChannelReqType::Break(rt),
         };
-        let p = ChannelRequest { num, want_reply, req: ty }.into();
-        Ok(p)
+
+        let p = ChannelRequest {
+            num: ch.send_num()?,
+            // we aren't handling responses for anything
+            want_reply: false,
+            req,
+        };
+        let p: Packet = p.into();
+        s.send(p)
     }
 }
 
+/// Convenience for the types of session channels that can be opened
+pub enum SessionCommand<S: AsRef<str>> {
+    Shell,
+    Exec(S),
+    Subsystem(S),
+}
+
+impl<'a, S: AsRef<str> + 'a> Into<Req<'a>> for &'a SessionCommand<S> {
+    fn into(self) -> Req<'a> {
+        match self  {
+            SessionCommand::Shell => Req::Shell,
+            SessionCommand::Exec(s) => Req::Exec(s.as_ref()),
+            SessionCommand::Subsystem(s) => Req::Subsystem(s.as_ref()),
+        }
+    }
+}
+
+
 // // Variants match packets::ChannelReqType, without data
 // enum ReqKind {
 //     Shell,
@@ -550,11 +565,6 @@ impl Req {
 //     Break,
 // }
 
-// shell+pty. or perhaps this should match the hook queue size and then
-// we can stop servicing the hook queue if this limit is reached.
-// const MAX_OUTSTANDING_REQS: usize = 2;
-const MAX_INIT_REQS: usize = 2;
-
 /// Per-direction channel variables
 #[derive(Debug)]
 struct ChanDir {
@@ -572,17 +582,9 @@ enum ChanState {
     /// Not to be used for normal channel messages
     InOpen,
 
-    /// `init_req` are the request messages to be sent once the ChannelOpenConfirmation
-    /// is received
-
-    // TODO: this is wasting half a kB. where else could we store it? could
-    // the Behaviour own it? Or we don't store them here, just callback to the Behaviour.
-
     // TODO: perhaps .get() and .get_mut() should ignore Opening state channels?
 
-    Opening {
-        init_req: InitReqs,
-    },
+    Opening,
     Normal,
     RecvEof,
     // TODO: recvclose state probably shouldn't be possible, we remove it straight away?
@@ -615,10 +617,10 @@ pub(crate) struct Channel {
 }
 
 impl Channel {
-    fn new(num: ChanNum, ty: ChanType, init_req: InitReqs) -> Self {
+    fn new(num: ChanNum, ty: ChanType) -> Self {
         Channel {
             ty,
-            state: ChanState::Opening { init_req },
+            state: ChanState::Opening,
             sent_close: false,
             sent_eof: false,
             // last_req: Deque::new(),
@@ -647,12 +649,6 @@ impl Channel {
         Ok(self.send.as_ref().trap()?.num)
     }
 
-    fn request(&self, req: ReqDetails, s: &mut TrafSend) -> Result<()> {
-        let num = self.send_num()?;
-        let r = Req { num, details: req };
-        s.send(r.packet()?)
-    }
-
     /// Returns an open confirmation reply packet to send.
     /// Must be called with state of `InOpen`.
     fn open_done<'p>(&mut self) -> Result<Packet<'p>> {
@@ -838,20 +834,6 @@ impl Channel {
     }
 }
 
-pub struct ChanMsg {
-    pub num: ChanNum,
-    pub msg: ChanMsgDetails,
-}
-
-pub enum ChanMsgDetails {
-    Data,
-    ExtData { ext: u32 },
-    // TODO: perhaps we don't need the storaged ReqDetails, just have the reqtype packet?
-    Req(ReqDetails),
-    // TODO closein/closeout/eof, etc. Should also return the exit status etc
-    Close,
-}
-
 #[derive(Debug)]
 pub(crate) struct DataIn {
     pub num: ChanNum,
@@ -927,8 +909,6 @@ impl ChanData {
     }
 }
 
-pub(crate) type InitReqs = Vec<ReqDetails, MAX_INIT_REQS>;
-
 // for dispatch_open_inner()
 enum DispatchOpenError {
     /// A program error
@@ -952,3 +932,44 @@ impl From<ChanFail> for DispatchOpenError {
     }
 }
 
+pub struct SessionOpener<'a, 's, 't> {
+    ch: &'a Channel,
+    s: &'a mut TrafSend<'s, 't>,
+}
+
+impl<'a, 's, 't> SessionOpener<'a, 's, 't> {
+    fn new(ch: &'a Channel, s: &'a mut TrafSend<'s, 't>) -> Self {
+        Self {
+            ch,
+            s,
+        }
+    }
+
+    pub fn cmd<S: AsRef<str>>(&mut self, cmd: &SessionCommand<S>) {
+        self.send(cmd.into())
+    }
+
+    pub fn shell(&mut self) {
+        self.send(Req::Shell)
+    }
+
+    pub fn exec(&mut self, cmd: &str) {
+        self.send(Req::Exec(cmd))
+    }
+
+    pub fn subsystem(&mut self, cmd: &str) {
+        self.send(Req::Subsystem(cmd))
+    }
+
+    pub fn pty(&mut self, pty: channel::Pty) {
+        self.send(Req::Pty(pty))
+    }
+
+    fn send(&mut self, req: Req) {
+        let r = req.send(self.ch, self.s);
+        if let Err(e) = r {
+            warn!("Error sending request: {e:?}")
+        }
+    }
+}
+
diff --git a/src/conn.rs b/src/conn.rs
index f628bb0baabd2094085b31bc706c2c1efc6b3d77..a2502a3c027a2216f8a162daef2bd20c4a51a044 100644
--- a/src/conn.rs
+++ b/src/conn.rs
@@ -325,7 +325,6 @@ impl<C: CliBehaviour, S: ServBehaviour> Conn<C, S> {
                 }
             }
             Packet::UserauthBanner(p) => {
-                // TODO: client only
                 if let ClientServer::Client(cli) = &mut self.cliserv {
                     cli.banner(&p, b.client()?);
                 } else {
diff --git a/src/ident.rs b/src/ident.rs
index 2863e19ee5db4a5994cdac01acdf002c085c2bb0..256904143250068b2a6b904343512822211eb9ff 100644
--- a/src/ident.rs
+++ b/src/ident.rs
@@ -1,6 +1,6 @@
 use crate::error::{Error,TrapBug, Result};
 
-pub(crate) const OUR_VERSION: &[u8] = b"SSH-2.0-Sunset-0.1";
+pub(crate) const OUR_VERSION: &[u8] = b"SSH-2.0-Sunset-1";
 
 pub(crate) const SSH_PREFIX: &[u8] = b"SSH-2.0-";
 
diff --git a/src/lib.rs b/src/lib.rs
index 7b0ba60a7251850f2c1048407f97f3dee8fe576e..a8b053683acd12ed3216cb63560b78130f74fafb 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -36,7 +36,7 @@ pub mod sunsetlog;
 mod auth;
 mod channel;
 mod runner;
-// TODO only public for UnusedCli etc. 
+// TODO only public for UnusedCli etc.
 pub mod behaviour;
 mod termmodes;
 mod ssh_chapoly;
@@ -55,7 +55,7 @@ pub use runner::Runner;
 pub use sign::{SignKey, KeyType, OwnedSig};
 pub use packets::{PubKey, Signature};
 pub use error::{Error,Result};
-pub use channel::{ChanMsg, ChanMsgDetails, Pty, ChanOpened};
+pub use channel::{Pty, ChanOpened, SessionOpener, SessionCommand};
 pub use sshnames::ChanFail;
 pub use channel::{ChanData, ChanNum};
 pub use runner::ChanHandle;
diff --git a/src/namelist.rs b/src/namelist.rs
index 05644e1d2999f344943e12147ff5d9c710a65d33..49460c44a1ece2c6b6d5f2a0c028b2be4fa21a7c 100644
--- a/src/namelist.rs
+++ b/src/namelist.rs
@@ -233,7 +233,7 @@ mod tests {
             let n = LocalNames::try_from(*t).unwrap();
             let n = NameList::Local(&n);
             let mut buf = vec![99; 30];
-            let l = sshwire::write_ssh(&mut buf, &n, None).unwrap();
+            let l = sshwire::write_ssh(&mut buf, &n).unwrap();
             buf.truncate(l);
             let out1 = core::str::from_utf8(&buf).unwrap();
             // check that a join with std gives the same result.
diff --git a/src/packets.rs b/src/packets.rs
index 7bbb69b2bdc4de7eaf1153ca621e3afef7972450..c4f58b26b2cf6e3870718827853a13d2f8609f94 100644
--- a/src/packets.rs
+++ b/src/packets.rs
@@ -590,6 +590,11 @@ pub struct Exec<'a> {
     pub command: TextString<'a>,
 }
 
+#[derive(Debug, SSHEncode, SSHDecode)]
+pub struct Subsystem<'a> {
+    pub subsystem: TextString<'a>,
+}
+
 /// The contents of a `"pty-req"` request.
 ///
 /// Note that most function arguments use [`channel::Pty`] rather than this struct.
@@ -603,11 +608,6 @@ pub struct PtyReq<'a> {
     pub modes: BinString<'a>,
 }
 
-#[derive(Debug, SSHEncode, SSHDecode)]
-pub struct Subsystem<'a> {
-    pub subsystem: &'a str,
-}
-
 #[derive(Debug, Clone, SSHEncode, SSHDecode)]
 pub struct WinChange {
     pub cols: u32,
diff --git a/src/runner.rs b/src/runner.rs
index b875364ef7259b0e603e0f0d50fbb28496b203c5..04c29bcc5be2fbc5f9ebc8e7ccfbd28697474d58 100644
--- a/src/runner.rs
+++ b/src/runner.rs
@@ -8,7 +8,7 @@ use core::task::{Poll, Waker};
 
 use pretty_hex::PrettyHex;
 
-use crate::*;
+use crate::{*, packets::Subsystem};
 use packets::{ChannelDataExt, ChannelData};
 use crate::channel::{ChanNum, ChanData};
 use encrypt::KeyState;
@@ -208,20 +208,26 @@ impl<'a, C: CliBehaviour, S: ServBehaviour> Runner<'a, C, S> {
     }
 
     // TODO: move somewhere client specific?
-    pub fn open_client_session(&mut self, exec: Option<&str>, pty: Option<channel::Pty>) -> Result<ChanHandle> {
+    pub fn open_client_session(&mut self) -> Result<ChanHandle> {
         trace!("open_client_session");
-        let mut init_req = channel::InitReqs::new();
-        if let Some(pty) = pty {
-            init_req.push(channel::ReqDetails::Pty(pty)).trap()?;
-        }
-        if let Some(cmd) = exec {
-            let mut s = channel::ExecString::new();
-            s.push_str(cmd).trap()?;
-            init_req.push(channel::ReqDetails::Exec(s)).trap()?;
-        } else {
-            init_req.push(channel::ReqDetails::Shell).trap()?;
-        }
-        let (chan, p) = self.conn.channels.open(packets::ChannelOpenType::Session, init_req)?;
+        // let mut init_req = channel::InitReqs::new();
+        // if let Some(pty) = pty {
+        //     init_req.push(channel::ReqDetails::Pty(pty)).trap()?;
+        // }
+
+        // match cmd {
+        //     SessionCommand::Shell => {
+        //         init_req.push(channel::ReqDetails::Shell).trap()?;
+        //     }
+        // }
+        // if let Some(cmd) = exec {
+        //     let mut s = channel::ExecString::new();
+        //     s.push_str(cmd).trap()?;
+        //     init_req.push(channel::ReqDetails::Exec(s)).trap()?;
+        // } else {
+        // }
+
+        let (chan, p) = self.conn.channels.open(packets::ChannelOpenType::Session)?;
         self.traf_out.send_packet(p, &mut self.keys)?;
         self.wake();
         Ok(ChanHandle(chan))
diff --git a/src/sshwire.rs b/src/sshwire.rs
index 8509a1b8907d2377bcceb17c62b42d1838ad3400..f62c5d775c62690f81df91fe0034a041f999dec9 100644
--- a/src/sshwire.rs
+++ b/src/sshwire.rs
@@ -13,7 +13,7 @@ use {
 
 use core::str;
 use core::convert::AsRef;
-use core::fmt::{self,Debug};
+use core::fmt::{self,Debug,Display};
 use digest::Output;
 use pretty_hex::PrettyHex;
 use snafu::{prelude::*, Location};
@@ -362,6 +362,17 @@ impl Debug for TextString<'_> {
     }
 }
 
+impl Display for TextString<'_> {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        let s = core::str::from_utf8(self.0);
+        if let Ok(s) = s {
+            write!(f, "\"{}\"", s.escape_default())
+        } else {
+            write!(f, "{:?}", self)
+        }
+    }
+}
+
 impl SSHEncode for TextString<'_> {
     fn enc<S>(&self, s: &mut S) -> WireResult<()>
     where S: sshwire::SSHSink {
diff --git a/sshwire-derive/src/lib.rs b/sshwire-derive/src/lib.rs
index 8d843c3e2e9834e52df1f792c9f9916d102222a2..a871a43e3795b27c41c2f60f9a8ee7b44f649f46 100644
--- a/sshwire-derive/src/lib.rs
+++ b/sshwire-derive/src/lib.rs
@@ -77,10 +77,12 @@ enum FieldAtt {
     /// A variant method name will be encoded/decoded before the next field.
     /// eg `#[sshwire(variant_name = ch)]` for `ChannelRequest`
     VariantName(Ident),
+
     /// Any unknown variant name should be recorded here.
     /// This variant can't be written out.
     /// `#[sshwire(unknown))]`
     CaptureUnknown,
+
     /// The name of a variant, used by the parent struct
     /// `#[sshwire(variant = "exit-signal"))]`
     /// or
@@ -310,6 +312,8 @@ fn encode_enum(
                 }
                 Ok(())
             })?;
+            // an enum with only an Unknown variant will always return an earlier error
+            fn_body.push_parsed("#[allow(unreachable_code)]")?;
             fn_body.push_parsed("Ok(())")?;
             Ok(())
         })?;
@@ -380,7 +384,10 @@ fn encode_enum_names(
                 }
                 Ok(())
             })?;
-            fn_body.push_parsed("; Ok(r)")?;
+            fn_body.push_parsed(";")?;
+            // an enum with only an Unknown variant will always return an earlier error
+            fn_body.push_parsed("#[allow(unreachable_code)]")?;
+            fn_body.push_parsed("Ok(r)")?;
 
             Ok(())
         })?;