diff --git a/async/src/cmdline_client.rs b/async/src/cmdline_client.rs
index f7d67effa6d9cb30456bd41ed5f9172d30abb857..2f1c062fcfb3271d53b2afe45273e0b7de907164 100644
--- a/async/src/cmdline_client.rs
+++ b/async/src/cmdline_client.rs
@@ -35,11 +35,11 @@ enum CmdlineState<'a> {
     PreAuth,
     Authed,
     Opening {
-        io: ChanInOut<'a, CmdlineHooks<'a>, UnusedServ>,
-        extin: Option<ChanIn<'a, CmdlineHooks<'a>, UnusedServ>>,
+        io: ChanInOut<'a>,
+        extin: Option<ChanIn<'a>>,
     },
     Ready {
-        io: ChanInOut<'a, CmdlineHooks<'a>, UnusedServ>,
+        io: ChanInOut<'a>,
     },
 }
 
@@ -184,8 +184,8 @@ impl<'a> CmdlineRunner<'a> {
         }
     }
 
-    async fn chan_run(io: ChanInOut<'a, CmdlineHooks<'a>, UnusedServ>,
-        io_err: Option<ChanIn<'a, CmdlineHooks<'a>, UnusedServ>>,
+    async fn chan_run(io: ChanInOut<'a>,
+        io_err: Option<ChanIn<'a>>,
         pty_guard: Option<RawPtyGuard>) -> Result<()> {
         // out
         let fo = async {
@@ -318,7 +318,7 @@ impl<'a> CmdlineRunner<'a> {
     ///
     /// Performs authentication, requests a shell or command, performs channel IO.
     /// Will return `Ok` after the session ends normally, or an error.
-    pub async fn run(&mut self, cli: &'a SSHClient<'a, CmdlineHooks<'a>>) -> Result<()> {
+    pub async fn run(&mut self, cli: &'a SSHClient<'a>) -> Result<()> {
         // chanio is only set once a channel is opened below
         let chanio = Fuse::terminated();
         pin_mut!(chanio);
@@ -385,7 +385,7 @@ impl<'a> CmdlineRunner<'a> {
         Ok(())
     }
 
-    async fn open_session(&mut self, cli: &'a SSHClient<'a, CmdlineHooks<'a>>) -> Result<()> {
+    async fn open_session(&mut self, cli: &'a SSHClient<'a>) -> Result<()> {
         debug_assert!(matches!(self.state, CmdlineState::Authed));
 
         let (io, extin) = if self.want_pty {
diff --git a/embassy/demos/common/src/server.rs b/embassy/demos/common/src/server.rs
index 3cd858f443fe411691875c60379da9c9117373a0..f6160bbabda1816755369bf5daff40b4d8e07d76 100644
--- a/embassy/demos/common/src/server.rs
+++ b/embassy/demos/common/src/server.rs
@@ -228,7 +228,7 @@ pub trait DemoServer {
     /// A task to run for each incoming connection.
     // TODO: eventually the compiler should add must_use automatically?
     #[must_use]
-    async fn run<'f, S: ServBehaviour>(&self, serv: &'f SSHServer<'f, S>) -> Result<()>;
+    async fn run<'f>(&self, serv: &'f SSHServer<'f>) -> Result<()>;
 }
 
 
diff --git a/embassy/demos/picow/src/main.rs b/embassy/demos/picow/src/main.rs
index 7ce795becd799e2161776de1d7116c8cc2db2377..e83ba6af575bc9e55636dd8fe8ff63225b94e70d 100644
--- a/embassy/demos/picow/src/main.rs
+++ b/embassy/demos/picow/src/main.rs
@@ -323,9 +323,9 @@ impl DemoServer for PicoServer {
         *u = username.try_into().unwrap_or(String::new());
     }
 
-    async fn run<'f, S: ServBehaviour>(
+    async fn run<'f>(
         &self,
-        serv: &'f SSHServer<'f, S>,
+        serv: &'f SSHServer<'f>,
     ) -> Result<()> {
         let session = async {
             // wait for a shell to start
diff --git a/embassy/demos/std/src/main.rs b/embassy/demos/std/src/main.rs
index 8188b1a499bc2ede460f65972c327b08aefc5fe3..e0c62cc288816d5a835f23961f22eea5da862d01 100644
--- a/embassy/demos/std/src/main.rs
+++ b/embassy/demos/std/src/main.rs
@@ -88,7 +88,7 @@ impl DemoServer for StdDemo {
         self.notify.signal(handle);
     }
 
-    async fn run<'f, S: ServBehaviour>(&self, serv: &'f SSHServer<'f, S>) -> Result<()>
+    async fn run<'f>(&self, serv: &'f SSHServer<'f>) -> Result<()>
     {
         let session = async {
             // wait for a shell to start
diff --git a/embassy/src/client.rs b/embassy/src/client.rs
index ca7c77ae2f9a3aa0d06bfe5027c7a66ce1f09e7a..91e85d15efe30adebc8ad3a82dceb87b306ff934 100644
--- a/embassy/src/client.rs
+++ b/embassy/src/client.rs
@@ -10,13 +10,21 @@ use sunset::ChanData;
 use embassy_sunset::EmbassySunset;
 use embassy_channel::{ChanInOut, ChanIn};
 
-pub struct SSHClient<'a, C: CliBehaviour> {
-    sunset: EmbassySunset<'a, C, UnusedServ>,
+/// An async SSH client instance 
+///
+/// The [`run()`][Self::run] method runs the session to completion, with application behaviour
+/// defined by the [`CliBehaviour`] instance.
+///
+/// Once authentication has completed ([`authenticated()`][CliBehaviour] is called), the application
+/// may open remote channels with [`open_session_pty()`][Self::open_session_pty] etc.
+///
+/// This is async executor agnostic, though requires the `CliBehaviour` instance
+/// to be wrapped in an Embassy [`Mutex`].
+pub struct SSHClient<'a> {
+    sunset: EmbassySunset<'a>,
 }
 
-type S = UnusedServ;
-
-impl<'a, C: CliBehaviour> SSHClient<'a, C> {
+impl<'a> SSHClient<'a> {
     pub fn new(inbuf: &'a mut [u8], outbuf: &'a mut [u8],
         ) -> Result<Self> {
         let runner = Runner::new_client(inbuf, outbuf)?;
@@ -24,12 +32,12 @@ impl<'a, C: CliBehaviour> SSHClient<'a, C> {
         Ok(Self { sunset })
     }
 
-    pub async fn run<B: ?Sized, M: RawMutex>(&self,
+    pub async fn run<B: ?Sized, M: RawMutex, C: CliBehaviour>(&self,
         rsock: &mut impl Read,
         wsock: &mut impl Write,
         b: &Mutex<M, B>) -> Result<()>
         where
-            for<'f> Behaviour<'f, C, S>: From<&'f mut B>
+            for<'f> Behaviour<'f, C, UnusedServ>: From<&'f mut B>
     {
         self.sunset.run(rsock, wsock, b).await
     }
@@ -39,7 +47,7 @@ impl<'a, C: CliBehaviour> SSHClient<'a, C> {
     }
 
     pub async fn open_session_nopty(&'a self)
-    -> Result<(ChanInOut<'a, C, S>, ChanIn<'a, C, S>)> {
+    -> Result<(ChanInOut<'a>, ChanIn<'a>)> {
         let chan = self.sunset.with_runner(|runner| {
             runner.open_client_session()
         }).await?;
@@ -52,7 +60,7 @@ impl<'a, C: CliBehaviour> SSHClient<'a, C> {
         Ok((cstd, cerr))
     }
 
-    pub async fn open_session_pty(&'a self) -> Result<ChanInOut<'a, C, S>> {
+    pub async fn open_session_pty(&'a self) -> Result<ChanInOut<'a>> {
         let chan = self.sunset.with_runner(|runner| {
             runner.open_client_session()
         }).await?;
diff --git a/embassy/src/embassy_channel.rs b/embassy/src/embassy_channel.rs
index a9e3b83e242cbea85e9d1ffd5c91da48625005f4..33b0cce060f5c1dd2b698540af8d3a887ff98e08 100644
--- a/embassy/src/embassy_channel.rs
+++ b/embassy/src/embassy_channel.rs
@@ -9,16 +9,16 @@ use embedded_io_async::{Read, Write, ErrorType};
 
 use crate::*;
 use embassy_sunset::EmbassySunset;
-use sunset::{Result, ChanData, ChanNum, CliBehaviour, ServBehaviour};
+use sunset::{Result, ChanData, ChanNum};
 
 /// Common implementation
-struct ChanIO<'a, C: CliBehaviour, S: ServBehaviour> {
+struct ChanIO<'a> {
     num: ChanNum,
     dt: ChanData,
-    sunset: &'a EmbassySunset<'a, C, S>,
+    sunset: &'a EmbassySunset<'a>,
 }
 
-impl<C: CliBehaviour, S: ServBehaviour> core::fmt::Debug for ChanIO<'_, C, S> {
+impl core::fmt::Debug for ChanIO<'_> {
     fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
         f.debug_struct("ChanIO")
             .field("num", &self.num)
@@ -27,19 +27,19 @@ impl<C: CliBehaviour, S: ServBehaviour> core::fmt::Debug for ChanIO<'_, C, S> {
     }
 }
 
-impl<C: CliBehaviour, S: ServBehaviour> ChanIO<'_, C, S> {
+impl ChanIO<'_> {
     pub async fn until_closed(&self) -> Result<()> {
         self.sunset.until_channel_closed(self.num).await
     }
 }
 
-impl<C: CliBehaviour, S: ServBehaviour> Drop for ChanIO<'_, C, S> {
+impl Drop for ChanIO<'_> {
     fn drop(&mut self) {
         self.sunset.dec_chan(self.num)
     }
 }
 
-impl<C: CliBehaviour, S: ServBehaviour> Clone for ChanIO<'_, C, S> {
+impl Clone for ChanIO<'_> {
     fn clone(&self) -> Self {
         self.sunset.inc_chan(self.num);
         Self {
@@ -50,17 +50,17 @@ impl<C: CliBehaviour, S: ServBehaviour> Clone for ChanIO<'_, C, S> {
     }
 }
 
-impl<C: CliBehaviour, S: ServBehaviour> ErrorType for ChanIO<'_, C, S> {
+impl ErrorType for ChanIO<'_> {
     type Error = sunset::Error;
 }
 
-impl<'a, C: CliBehaviour, S: ServBehaviour> Read for ChanIO<'a, C, S> {
+impl<'a> Read for ChanIO<'a> {
     async fn read(&mut self, buf: &mut [u8]) -> Result<usize, sunset::Error> {
         self.sunset.read_channel(self.num, self.dt, buf).await
     }
 }
 
-impl<'a, C: CliBehaviour, S: ServBehaviour> Write for ChanIO<'a, C, S> {
+impl<'a> Write for ChanIO<'a> {
     async fn write(&mut self, buf: &[u8]) -> Result<usize, sunset::Error> {
         self.sunset.write_channel(self.num, self.dt, buf).await
     }
@@ -74,37 +74,37 @@ impl<'a, C: CliBehaviour, S: ServBehaviour> Write for ChanIO<'a, C, S> {
 
 /// A standard bidirectional SSH channel
 #[derive(Debug)]
-pub struct ChanInOut<'a, C: CliBehaviour, S: ServBehaviour>(ChanIO<'a, C, S>);
+pub struct ChanInOut<'a>(ChanIO<'a>);
 
 /// An input-only SSH channel, such as stderr for a client
 #[derive(Debug)]
-pub struct ChanIn<'a, C: CliBehaviour, S: ServBehaviour>(ChanIO<'a, C, S>);
+pub struct ChanIn<'a>(ChanIO<'a>);
 
 #[derive(Debug)]
 /// An output-only SSH channel, such as stderr for a server
-pub struct ChanOut<'a, C: CliBehaviour, S: ServBehaviour>(ChanIO<'a, C, S>);
+pub struct ChanOut<'a>(ChanIO<'a>);
 
 // derive(Clone) adds unwanted `: Clone` bounds on C and S, so we implement manually
 // https://github.com/rust-lang/rust/issues/26925
-impl<'a, C: CliBehaviour, S: ServBehaviour> Clone for ChanInOut<'a, C, S> {
+impl<'a> Clone for ChanInOut<'a> {
     fn clone(&self) -> Self {
         Self(self.0.clone())
     }
 }
-impl<'a, C: CliBehaviour, S: ServBehaviour> Clone for ChanIn<'a, C, S> {
+impl<'a> Clone for ChanIn<'a> {
     fn clone(&self) -> Self {
         Self(self.0.clone())
     }
 }
-impl<'a, C: CliBehaviour, S: ServBehaviour> Clone for ChanOut<'a, C, S> {
+impl<'a> Clone for ChanOut<'a> {
     fn clone(&self) -> Self {
         Self(self.0.clone())
     }
 }
 
-impl<'a, C: CliBehaviour, S: ServBehaviour> ChanInOut<'a, C, S> {
+impl<'a> ChanInOut<'a> {
     // caller must have already incremented the refcount
-    pub(crate) fn new(num: ChanNum, dt: ChanData, sunset: &'a EmbassySunset<'a, C, S>) -> Self {
+    pub(crate) fn new(num: ChanNum, dt: ChanData, sunset: &'a EmbassySunset<'a>) -> Self {
         Self(ChanIO {
             num, dt, sunset,
         })
@@ -123,18 +123,18 @@ impl<'a, C: CliBehaviour, S: ServBehaviour> ChanInOut<'a, C, S> {
     }
 }
 
-impl<'a, C: CliBehaviour, S: ServBehaviour> ChanIn<'a, C, S> {
+impl<'a> ChanIn<'a> {
     // caller must have already incremented the refcount
-    pub(crate) fn new(num: ChanNum, dt: ChanData, sunset: &'a EmbassySunset<'a, C, S>) -> Self {
+    pub(crate) fn new(num: ChanNum, dt: ChanData, sunset: &'a EmbassySunset<'a>) -> Self {
         Self(ChanIO {
             num, dt, sunset,
         })
     }
 }
 
-impl<'a, C: CliBehaviour, S: ServBehaviour> ChanOut<'a, C, S> {
+impl<'a> ChanOut<'a> {
     // caller must have already incremented the refcount
-    pub(crate) fn new(num: ChanNum, dt: ChanData, sunset: &'a EmbassySunset<'a, C, S>) -> Self {
+    pub(crate) fn new(num: ChanNum, dt: ChanData, sunset: &'a EmbassySunset<'a>) -> Self {
         Self(ChanIO {
             num, dt, sunset,
         })
@@ -146,37 +146,37 @@ impl<'a, C: CliBehaviour, S: ServBehaviour> ChanOut<'a, C, S> {
     }
 }
 
-impl<C: CliBehaviour, S: ServBehaviour> ErrorType for ChanInOut<'_, C, S> {
+impl ErrorType for ChanInOut<'_> {
     type Error = sunset::Error;
 }
 
-impl<C: CliBehaviour, S: ServBehaviour> ErrorType for ChanIn<'_, C, S> {
+impl ErrorType for ChanIn<'_> {
     type Error = sunset::Error;
 }
 
-impl<C: CliBehaviour, S: ServBehaviour> ErrorType for ChanOut<'_, C, S> {
+impl ErrorType for ChanOut<'_> {
     type Error = sunset::Error;
 }
 
-impl<'a, C: CliBehaviour, S: ServBehaviour> Read for ChanInOut<'a, C, S> {
+impl<'a> Read for ChanInOut<'a> {
     async fn read(&mut self, buf: &mut [u8]) -> Result<usize, sunset::Error> {
         self.0.read(buf).await
     }
 }
 
-impl<'a, C: CliBehaviour, S: ServBehaviour> Write for ChanInOut<'a, C, S> {
+impl<'a> Write for ChanInOut<'a> {
     async fn write(&mut self, buf: &[u8]) -> Result<usize, sunset::Error> {
         self.0.write(buf).await
     }
 }
 
-impl<'a, C: CliBehaviour, S: ServBehaviour> Read for ChanIn<'a, C, S> {
+impl<'a> Read for ChanIn<'a> {
     async fn read(&mut self, buf: &mut [u8]) -> Result<usize, sunset::Error> {
         self.0.read(buf).await
     }
 }
 
-impl<'a, C: CliBehaviour, S: ServBehaviour> Write for ChanOut<'a, C, S> {
+impl<'a> Write for ChanOut<'a> {
     async fn write(&mut self, buf: &[u8]) -> Result<usize, sunset::Error> {
         self.0.write(buf).await
     }
diff --git a/embassy/src/embassy_sunset.rs b/embassy/src/embassy_sunset.rs
index f06dce788001e845f8971691ad53ba82655d604b..51e55cf055b58ed7b9a6f2c012033fac45d9bcc2 100644
--- a/embassy/src/embassy_sunset.rs
+++ b/embassy/src/embassy_sunset.rs
@@ -49,8 +49,8 @@ struct Wakers {
     chan_close: [WakerRegistration; MAX_CHANNELS],
 }
 
-struct Inner<'a, C: CliBehaviour, S: ServBehaviour> {
-    runner: Runner<'a, C, S>,
+struct Inner<'a> {
+    runner: Runner<'a>,
 
     wakers: Wakers,
 
@@ -59,11 +59,11 @@ struct Inner<'a, C: CliBehaviour, S: ServBehaviour> {
     chan_handles: [Option<ChanHandle>; MAX_CHANNELS],
 }
 
-impl<'a, C: CliBehaviour, S: ServBehaviour> Inner<'a, C, S> {
+impl<'a> Inner<'a> {
     /// Helper to lookup the corresponding ChanHandle
     ///
     /// Returns split references that will be required by many callers
-    fn fetch(&mut self, num: ChanNum) -> Result<(&mut Runner<'a, C, S>, &ChanHandle, &mut Wakers)> {
+    fn fetch(&mut self, num: ChanNum) -> Result<(&mut Runner<'a>, &ChanHandle, &mut Wakers)> {
         self.chan_handles[num.0 as usize].as_ref().map(|ch| {
             (&mut self.runner, ch, &mut self.wakers)
         })
@@ -71,14 +71,14 @@ impl<'a, C: CliBehaviour, S: ServBehaviour> Inner<'a, C, S> {
     }
 }
 
-/// Provides an async wrapper for sunset core
+/// Provides an async wrapper for Sunset core
 ///
-/// A `ChanHandle` provided by sunset core must be added with `add_channel()` before
+/// A [`ChanHandle`] provided by sunset core must be added with [`add_channel()`] before
 /// a method can be called with the equivalent ChanNum.
 ///
 /// Applications use `embassy_sunset::{Client,Server}`.
-pub(crate) struct EmbassySunset<'a, C: CliBehaviour, S: ServBehaviour> {
-    inner: Mutex<SunsetRawMutex, Inner<'a, C, S>>,
+pub(crate) struct EmbassySunset<'a> {
+    inner: Mutex<SunsetRawMutex, Inner<'a>>,
 
     progress_notify: Signal<SunsetRawMutex, ()>,
 
@@ -94,8 +94,8 @@ pub(crate) struct EmbassySunset<'a, C: CliBehaviour, S: ServBehaviour> {
     chan_refcounts: [AtomicUsize; MAX_CHANNELS],
 }
 
-impl<'a, C: CliBehaviour, S: ServBehaviour> EmbassySunset<'a, C, S> {
-    pub fn new(runner: Runner<'a, C, S>) -> Self {
+impl<'a> EmbassySunset<'a> {
+    pub fn new(runner: Runner<'a>) -> Self {
         let wakers = Wakers {
             chan_read: Default::default(),
             chan_write: Default::default(),
@@ -120,7 +120,7 @@ impl<'a, C: CliBehaviour, S: ServBehaviour> EmbassySunset<'a, C, S> {
          }
     }
 
-    pub async fn run<B: ?Sized, M: RawMutex>(&self,
+    pub async fn run<B: ?Sized, M: RawMutex, C: CliBehaviour, S: ServBehaviour>(&self,
         rsock: &mut impl Read,
         wsock: &mut impl Write,
         b: &Mutex<M, B>) -> Result<()>
@@ -224,7 +224,7 @@ impl<'a, C: CliBehaviour, S: ServBehaviour> EmbassySunset<'a, C, S> {
         self.wake_progress()
     }
 
-    fn wake_channels(&self, inner: &mut Inner<C, S>) -> Result<()> {
+    fn wake_channels(&self, inner: &mut Inner) -> Result<()> {
         // Read wakers
         let w = &mut inner.wakers;
         if let Some((num, dt, _len)) = inner.runner.ready_channel_input() {
@@ -278,7 +278,7 @@ impl<'a, C: CliBehaviour, S: ServBehaviour> EmbassySunset<'a, C, S> {
     /// without "async Drop" it isn't possible to take the `inner` lock during
     /// `drop()`.
     /// Instead this runs periodically from an async context to release channels.
-    fn clear_refcounts(&self, inner: &mut Inner<C, S>) -> Result<()> {
+    fn clear_refcounts(&self, inner: &mut Inner) -> Result<()> {
         for (ch, count) in inner.chan_handles.iter_mut().zip(self.chan_refcounts.iter()) {
             let count = count.load(Relaxed);
             if count > 0 {
@@ -294,7 +294,7 @@ impl<'a, C: CliBehaviour, S: ServBehaviour> EmbassySunset<'a, C, S> {
     }
 
     /// Returns ControlFlow::Break on session exit.
-    async fn progress<B: ?Sized, M: RawMutex>(&self,
+    async fn progress<B: ?Sized, M: RawMutex, C: CliBehaviour, S: ServBehaviour>(&self,
         b: &Mutex<M, B>)
         -> Result<ControlFlow<()>>
         where
@@ -342,14 +342,14 @@ impl<'a, C: CliBehaviour, S: ServBehaviour> EmbassySunset<'a, C, S> {
     }
 
     pub(crate) async fn with_runner<F, R>(&self, f: F) -> R
-        where F: FnOnce(&mut Runner<C, S>) -> R {
+        where F: FnOnce(&mut Runner) -> R {
         let mut inner = self.inner.lock().await;
         f(&mut inner.runner)
     }
 
     /// helper to perform a function on the `inner`, returning a `Poll` value
     async fn poll_inner<F, T>(&self, mut f: F) -> T
-        where F: FnMut(&mut Inner<C, S>, &mut Context) -> Poll<T> {
+        where F: FnMut(&mut Inner, &mut Context) -> Poll<T> {
         poll_fn(|cx| {
             // Attempt to lock .inner
             let i = self.inner.lock();
diff --git a/embassy/src/server.rs b/embassy/src/server.rs
index 5d0e1ae9a82172cfd5aa4594502a3fff0c0f886f..88b1dc32658313bc71970278f963325d2b69bbd7 100644
--- a/embassy/src/server.rs
+++ b/embassy/src/server.rs
@@ -8,13 +8,21 @@ use sunset::behaviour::UnusedCli;
 use crate::*;
 use embassy_sunset::EmbassySunset;
 
-pub struct SSHServer<'a, S: ServBehaviour> {
-    sunset: EmbassySunset<'a, UnusedCli, S>,
+/// An async SSH server instance 
+///
+/// The [`run()`][Self::run] method runs the session to completion, with application behaviour
+/// defined by the [`ServBehaviour`] instance.
+///
+/// Once the client has opened sessions, those can be retrieved with [`stdio()`][Self::stdio]
+/// and [`stdio_stderr()`][Self::stdio_stderr] methods.
+///
+/// This is async executor agnostic, though requires the `ServBehaviour` instance
+/// to be wrapped in an Embassy [`Mutex`].
+pub struct SSHServer<'a> {
+    sunset: EmbassySunset<'a>,
 }
 
-type C = UnusedCli;
-
-impl<'a, S: ServBehaviour> SSHServer<'a, S> {
+impl<'a> SSHServer<'a> {
     // May return an error if RNG fails
     pub fn new(inbuf: &'a mut [u8], outbuf: &'a mut [u8],
         ) -> Result<Self> {
@@ -23,7 +31,7 @@ impl<'a, S: ServBehaviour> SSHServer<'a, S> {
         Ok(Self { sunset })
     }
 
-    pub async fn run<B: ?Sized, M: RawMutex>(&self,
+    pub async fn run<B: ?Sized, M: RawMutex, S: ServBehaviour>(&self,
         rsock: &mut impl Read,
         wsock: &mut impl Write,
         b: &Mutex<M, B>) -> Result<()>
@@ -39,14 +47,20 @@ impl<'a, S: ServBehaviour> SSHServer<'a, S> {
     /// data type.
     /// `ch` is the [`ChanHandle`] passed to the application's `Behaviour`
     /// methods.
-    pub async fn stdio(&'a self, ch: ChanHandle) -> Result<ChanInOut<'a, C, S>> {
+    pub async fn stdio(&'a self, ch: ChanHandle) -> Result<ChanInOut<'a>> {
         let num = ch.num();
         self.sunset.add_channel(ch, 1).await?;
         Ok(ChanInOut::new(num, ChanData::Normal, &self.sunset))
     }
 
+    /// Retrieve the stdin/stdout/stderr streams.
+    ///
+    /// If stderr is not required, use [`stdio()`][Self::stdio] instead to avoid needing to poll
+    /// the returned stderr.
+    /// The session will block until the streams are drained (they use the session buffer),
+    /// so they must be drained if used.
     pub async fn stdio_stderr(&'a self, ch: ChanHandle)
-        -> Result<(ChanInOut<'a, C, S>, ChanOut<'a, C, S>)> {
+        -> Result<(ChanInOut<'a>, ChanOut<'a>)> {
         let num = ch.num();
         self.sunset.add_channel(ch, 2).await?;
         let i = ChanInOut::new(num, ChanData::Normal, &self.sunset);
diff --git a/src/channel.rs b/src/channel.rs
index fd6e5f38f90f6157be09a593d62ef992b80b1e42..2dff752c799f8b8f953e61ac4e5fe1d9d439b4d2 100644
--- a/src/channel.rs
+++ b/src/channel.rs
@@ -22,16 +22,14 @@ use runner::ChanHandle;
 
 use snafu::ErrorCompat;
 
-pub(crate) struct Channels<C: CliBehaviour, S: ServBehaviour> {
+pub(crate) struct Channels {
     ch: [Option<Channel>; config::MAX_CHANNELS],
-    _ph: (PhantomData<C>, PhantomData<S>),
 }
 
-impl<C: CliBehaviour, S: ServBehaviour> Channels<C, S> {
+impl Channels {
     pub fn new() -> Self {
         Channels {
             ch: Default::default(),
-            _ph: Default::default()
         }
     }
 
@@ -219,7 +217,7 @@ impl<C: CliBehaviour, S: ServBehaviour> Channels<C, S> {
         }
     }
 
-    fn dispatch_open(
+    fn dispatch_open<C: CliBehaviour, S: ServBehaviour>(
         &mut self,
         p: &ChannelOpen<'_>,
         s: &mut TrafSend,
@@ -241,7 +239,7 @@ impl<C: CliBehaviour, S: ServBehaviour> Channels<C, S> {
     }
 
     // the caller will send failure messages if required
-    fn dispatch_open_inner(
+    fn dispatch_open_inner<C: CliBehaviour, S: ServBehaviour>(
         &mut self,
         p: &ChannelOpen<'_>,
         s: &mut TrafSend,
@@ -304,7 +302,7 @@ impl<C: CliBehaviour, S: ServBehaviour> Channels<C, S> {
     }
 
     // Some returned errors will be caught by caller and returned as SSH messages
-    async fn dispatch_inner(
+    async fn dispatch_inner<C: CliBehaviour, S: ServBehaviour>(
         &mut self,
         packet: Packet<'_>,
         is_client: bool,
@@ -417,7 +415,7 @@ impl<C: CliBehaviour, S: ServBehaviour> Channels<C, S> {
     /// Incoming packet handling
     // TODO: protocol errors etc should perhaps be less fatal,
     // ssh implementations are usually imperfect.
-    pub async fn dispatch(
+    pub async fn dispatch<C: CliBehaviour, S: ServBehaviour>(
         &mut self,
         packet: Packet<'_>,
         is_client: bool,
diff --git a/src/conn.rs b/src/conn.rs
index 2e193f70a3053bc90d49be561291c4b19e0ce4cb..16bd39c672dbdf1a048416eaf30886fbe88fcc5e 100644
--- a/src/conn.rs
+++ b/src/conn.rs
@@ -23,7 +23,7 @@ use config::MAX_CHANNELS;
 use kex::{Kex, SessId, AlgoConfig};
 
 /// The core state of a SSH instance.
-pub(crate) struct Conn<C: CliBehaviour, S: ServBehaviour> {
+pub(crate) struct Conn {
     state: ConnState,
 
     // State of any current Key Exchange
@@ -42,7 +42,7 @@ pub(crate) struct Conn<C: CliBehaviour, S: ServBehaviour> {
     // 256 bytes -> 112 bytes
     pub(crate) remote_version: ident::RemoteVersion,
 
-    pub(crate) channels: Channels<C, S>,
+    pub(crate) channels: Channels,
 }
 
 // TODO: what tricks can we do to optimise away client or server code if we only
@@ -84,7 +84,7 @@ pub(crate) struct Dispatched {
     pub disconnect: bool,
 }
 
-impl<C: CliBehaviour, S: ServBehaviour> Conn<C, S> {
+impl Conn {
     pub fn new_client() -> Result<Self> {
         let algo_conf = AlgoConfig::new(true);
         Self::new(ClientServer::Client(client::Client::new()), algo_conf)
@@ -114,7 +114,7 @@ impl<C: CliBehaviour, S: ServBehaviour> Conn<C, S> {
 
     /// Updates `ConnState` and sends any packets required to progress the connection state.
     // TODO can this just move to the bottom of handle_payload(), and make module-private?
-    pub(crate) async fn progress(
+    pub(crate) async fn progress<C: CliBehaviour, S: ServBehaviour>(
         &mut self,
         s: &mut TrafSend<'_, '_>,
         b: &mut Behaviour<'_, C, S>,
@@ -166,7 +166,7 @@ impl<C: CliBehaviour, S: ServBehaviour> Conn<C, S> {
     /// Consumes an input payload which is a view into [`traffic::Traffic::rxbuf`].
     /// We queue response packets that can be sent (written into the same buffer)
     /// after `handle_payload()` runs.
-    pub(crate) async fn handle_payload(
+    pub(crate) async fn handle_payload<C: CliBehaviour, S: ServBehaviour>(
         &mut self, payload: &[u8], seq: u32,
         s: &mut TrafSend<'_, '_>,
         b: &mut Behaviour<'_, C, S>,
@@ -255,7 +255,7 @@ impl<C: CliBehaviour, S: ServBehaviour> Conn<C, S> {
         self.sess_id.is_none()
     }
 
-    async fn dispatch_packet(
+    async fn dispatch_packet<C: CliBehaviour, S: ServBehaviour>(
         &mut self, packet: Packet<'_>, s: &mut TrafSend<'_, '_>, b: &mut Behaviour<'_, C, S>,
     ) -> Result<Dispatched, Error> {
         // TODO: perhaps could consolidate packet client vs server checks
diff --git a/src/runner.rs b/src/runner.rs
index b74031c5a635d29138ae278246fb1a3ae849ec61..1bcde5bc7521fa78ec68b384c35548ef7ba7646e 100644
--- a/src/runner.rs
+++ b/src/runner.rs
@@ -26,8 +26,8 @@ use conn::{Conn, Dispatched};
 ///
 /// An application provides network or channel data to `Runner` method calls,
 /// and provides customisation callbacks via `CliBehaviour` or `ServBehaviour`.
-pub struct Runner<'a, C: CliBehaviour, S: ServBehaviour> {
-    conn: Conn<C, S>,
+pub struct Runner<'a> {
+    conn: Conn,
 
     /// Binary packet handling from the network buffer
     traf_in: TrafIn<'a>,
@@ -45,7 +45,7 @@ pub struct Runner<'a, C: CliBehaviour, S: ServBehaviour> {
     closed: bool,
 }
 
-impl<C: CliBehaviour, S: ServBehaviour> core::fmt::Debug for Runner<'_, C, S> {
+impl core::fmt::Debug for Runner<'_> {
     fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
         f.debug_struct("Runner")
         .field("keys", &self.keys)
@@ -61,12 +61,12 @@ pub struct Progress {
     pub disconnected: bool,
 }
 
-impl<'a, C: CliBehaviour> Runner<'a, C, UnusedServ> {
-    /// `inbuf` must be sized to fit the largest SSH packet allowed.
+impl<'a> Runner<'a> {
+    /// `inbuf` and `outbuf` must be sized to fit the largest SSH packet allowed.
     pub fn new_client(
         inbuf: &'a mut [u8],
         outbuf: &'a mut [u8],
-    ) -> Result<Runner<'a, C, UnusedServ>, Error> {
+    ) -> Result<Runner<'a>, Error> {
         let conn = Conn::new_client()?;
         let runner = Runner {
             conn,
@@ -80,14 +80,12 @@ impl<'a, C: CliBehaviour> Runner<'a, C, UnusedServ> {
 
         Ok(runner)
     }
-}
-
 
-impl<'a, S: ServBehaviour> Runner<'a, UnusedCli, S> {
+    /// `inbuf` and `outbuf` must be sized to fit the largest SSH packet allowed.
     pub fn new_server(
         inbuf: &'a mut [u8],
         outbuf: &'a mut [u8],
-    ) -> Result<Runner<'a, UnusedCli, S>, Error> {
+    ) -> Result<Runner<'a>, Error> {
         let conn = Conn::new_server()?;
         let runner = Runner {
             conn,
@@ -101,9 +99,6 @@ impl<'a, S: ServBehaviour> Runner<'a, UnusedCli, S> {
 
         Ok(runner)
     }
-}
-
-impl<'a, C: CliBehaviour, S: ServBehaviour> Runner<'a, C, S> {
     pub fn is_client(&self) -> bool {
         self.conn.is_client()
     }
@@ -121,7 +116,7 @@ impl<'a, C: CliBehaviour, S: ServBehaviour> Runner<'a, C, S> {
     ///
     /// Returns `Ok(true)` if an input packet was handled, `Ok(false)` if no packet was ready
     /// (Can also return various errors)
-    pub async fn progress(&mut self, behaviour: &mut Behaviour<'_, C, S>)
+    pub async fn progress<C: CliBehaviour, S: ServBehaviour>(&mut self, behaviour: &mut Behaviour<'_, C, S>)
         -> Result<Progress>
     {
         let mut prog = Progress::default();