{"slug": "sesiones-remotas-siempre-disponibles-para-tus-agentes-de-codigo", "title": "Sesiones remotas siempre disponibles para tus agentes de codigo", "summary": "A developer shows how to run persistent remote coding agent sessions on a single small EC2 instance, surviving laptop closures and infrastructure changes. Claude Code offers native remote sessions via outbound-only connections, while Kiro CLI requires building a custom setup with tmux and SSM. The post details systemd units, credential management, and monitoring for both approaches.", "body_md": "Una sesión de agente que sobrevive a cerrar la laptop, corriendo en una sola instancia EC2 chica que manejas desde el teléfono. Este post cubre tanto Claude Code como Kiro CLI, porque resuelven el problema de \"alcanzar la sesión desde otro lado\" de maneras completamente distintas, y esa diferencia decide la mayor parte de tu arquitectura.\n\n| Versión ingenua | Después | |\n|---|---|---|\n| Las credenciales viven en | el volumen raíz de la instancia | Secrets Manager |\n| Sobrevive al reemplazo de la instancia | no, re-login manual | sí, sin atender (~2 min) |\n| Tiempo para notar una caja muerta |\n6 días (a mano) |\n15 min (alarma) |\n| Clone fallido al arrancar | tragado por `\\ | \\ |\n| Ciclo de vida acoplado a | el stack de producción | su propio stack |\n\nEl costo es una instancia ARM on-demand chica más un volumen gp3 de 30 GB: revisa el precio de lista actual de tu región, pero es la parte más barata de todo esto.\n\nUna instancia chica siempre encendida lo resuelve, con dos condiciones:\n\npuedes alcanzar la sesión desde donde estés, y la caja regresa por su\n\ncuenta cuando la infraestructura se mueve por debajo de ella. La segunda es de lo que este post trata en realidad.\n\nEsta es la bifurcación del camino, así que hazla bien antes de construir nada.\n\n**Claude Code tiene una sesión remota de primera mano.** {% raw %}`claude`\n\nregistra la sesión en curso con un punto de encuentro\n\nremote-control\n\nhospedado, y la manejas desde `claude.ai/code`\n\no la app móvil. La caja hace una conexión **de salida** nada más:\n\n```\nclaude remote-control --name myapp-cloud --continue\n```\n\n`--continue`\n\nreanuda la misma sesión a través de reinicios del proceso, así que un enlace en marcadores se queda estable cuando el servicio rebota.\n\nEsto necesita un CLI reciente: la caja de este post corría la 2.1.211. Al tener éxito imprime:\n\n```\nTake this session with you and pick up right where you left off on any device.\nOpen the Code tab in the Claude mobile app, or visit claude.ai/code in a browser.\nThe session keeps running on this machine. Use your other devices as a remote control. Press Ctrl+C to stop.\n```\n\nLa consecuencia de seguridad es grande: **sin puertos de entrada, sin SSH, sin listener público.** El security group puede ser solo de salida.\n\n**Kiro CLI no tiene equivalente.** Sus docs describen dos modos, ninguno de los cuales es una sesión remota:\n\n`kiro-cli chat --no-interactive \"prompt\"`\n\n, autenticado por `KIRO_API_KEY`\n\n. Los docs son explícitos en que \"No es posible input del usuario a media sesión\": un solo prompt, de principio a fin, sin reanudar.`kiro-cli chat --resume`\n\n(también `--resume-picker`\n\n, `--resume-id <ID>`\n\n, `--list-sessions`\n\n).Así que para Kiro la sesión siempre encendida es algo que *tú* construyes:\n\nmantén el proceso vivo en un multiplexor de terminal, y conéctate a él por SSM.\n\n```\n# en la caja, como el usuario de la app\ntmux new-session -d -s agent 'kiro-cli chat'\n\n# desde donde sea\naws ssm start-session --target \"$INSTANCE_ID\"\nsudo -iu appuser tmux attach -t agent\n```\n\n`--resume`\n\nes la red de seguridad más que el mecanismo: si el proceso se muere, la conversación sigue en disco, indexada por el directorio de trabajo.\n\nLectura práctica: en un teléfono, Claude Code gana de calle, una pestaña de navegador le gana a un shell de SSM móvil conectándose a tmux. El modelo de Kiro le queda mejor a una laptop o tablet con una terminal de verdad. El resto de este post aplica a los dos, porque las partes difíciles (credenciales, recuperación, monitoreo) son idénticas.\n\nUna unidad de systemd, y el proceso mismo es la sesión. Como la conexión es de salida, no se requiere nada más.\n\n```\nExecStart=/home/appuser/.local/bin/claude remote-control --name myapp-cloud --continue\nRestart=always\nRestartSec=30\n```\n\nLa salud es lo que sea que diga systemd: `systemctl is-active`\n\n. Esa es toda la integración.\n\nKiro necesita que lo durable sea la *terminal*, no el proceso del agente, porque no hay sesión que re-registrar. Corre tmux bajo systemd y deja que el agente viva adentro:\n\n```\n[Unit]\nDescription=Kiro agent session (tmux)\nAfter=network-online.target\nWants=network-online.target\nStartLimitIntervalSec=0\n\n[Service]\nType=forking\nUser=appuser\nWorkingDirectory=/home/appuser/myapp\nEnvironment=PATH=/home/appuser/.local/bin:/usr/local/bin:/usr/bin:/bin\nExecStart=/usr/bin/tmux new-session -d -s agent 'kiro-cli chat --resume'\nExecStop=/usr/bin/tmux kill-session -t agent\nRemainAfterExit=yes\nRestart=always\nRestartSec=30\n\n[Install]\nWantedBy=multi-user.target\n```\n\n`--resume`\n\nen `ExecStart`\n\nes deliberado: si la caja reinicia, la sesión nueva de tmux se reconecta a la conversación que ya está en disco para ese directorio en lugar de arrancar en frío.\n\nConéctate desde donde sea por SSM, todavía sin puertos de entrada:\n\n```\naws ssm start-session --target \"$INSTANCE_ID\"\nsudo -iu appuser tmux attach -t agent\n```\n\nLa salud significa algo distinto aquí, y este es el único lugar donde los dos caminos de verdad divergen. `systemctl is-active`\n\nsobre una unidad tmux de tipo forking te dice que tmux está vivo, no que el agente adentro lo esté. Pregúntale a tmux directo:\n\n```\nif sudo -u appuser tmux has-session -t agent 2>/dev/null; then\n  PANES=$(sudo -u appuser tmux list-panes -t agent -F '#{pane_dead}' | grep -c '^0$')\n  [ \"$PANES\" -gt 0 ] && heartbeat OK \"tmux:$PANES\" || fail \"tmux session has no live pane\"\nelse\n  fail \"tmux session missing\"\nfi\n```\n\n`#{pane_dead}`\n\nes el detalle que vale la pena guardar. Una sesión de tmux cuyo único panel ya salió sigue respondiendo `has-session`\n\ncon éxito, así que el chequeo ingenuo reporta sano una sesión con un agente muerto adentro, la misma clase de falso negativo que el timer inerte de más adelante.\n\nLa historia de credenciales de Kiro es más simple en un aspecto: la auth headless es una sola API key en `KIRO_API_KEY`\n\n, sin flujo de navegador y sin expiración de sesión, que cae directo en el mismo patrón de Secrets Manager. Vale la pena tenerla en la caja aunque manejes de forma interactiva, porque hace útil la caja para one-shots por script:\n\n```\nkiro-cli chat --no-interactive --trust-all-tools \"run the test suite and summarise failures\"\n```\n\nSé deliberado con `--trust-all-tools`\n\nen una máquina sin atender:\n\nauto-aprueba cada tool call. `--trust-tools=read,grep`\n\nes el default más seguro para cualquier cosa agendada.\n\nSi de plano prefieres no construir nada de esto para Kiro, AWS publica una [muestra Kiro IDE Remote de un clic](https://aws-samples.github.io/sample-one-click-generative-ai-solutions/en/solutions/kiro-ide/) que pone el IDE completo en un escritorio remoto alcanzable desde un navegador, con Kiro CLI y el AWS CLI preinstalados. Más pesada que una instancia chica, y un perfil de costo distinto, pero se salta el armado.\n\nLa caja original era una instancia EC2 definida dentro del stack del\n\nbackend de producción. El user-data instalaba el toolchain, clonaba el repo, escribía una unidad de systemd, y la dejaba **deshabilitada** a propósito: arrancar un agente sin autenticar nada más entra en crash-loop.\n\nUn humano luego se metía por SSM una vez, iniciaba sesión de forma\n\ninteractiva, y habilitaba el servicio.\n\n```\n// Dentro del stack del backend de producción. Tres errores separados.\nuserData.addCommands(\n  \"sudo -u appuser bash -lc 'curl -fsSL https://example-agent-installer.sh | bash' || true\",\n  `sudo -u appuser bash -lc 'test -d ~/myapp || git clone https://github.com/acme/myapp.git ~/myapp' || true`,\n  // ...archivo de unidad escrito aquí, a propósito sin habilitar...\n  \"systemctl daemon-reload\"\n);\n```\n\nEso funcionó por meses. Luego se movió el AMI.\n\nLa instancia usaba `MachineImage.fromSsmParameter(...)`\n\npara seguir la imagen actual de Ubuntu 24.04. Cuando ese parámetro resolvió un AMI más nuevo, CloudFormation vio cambiar el `ImageId`\n\n, lo cual es un **reemplazo**, no una actualización. Instancia nueva, volumen nuevo, y todo lo del volumen raíz viejo se fue.\n\nEl reemplazo arrancó y se veía bien. No lo estaba:\n\n```\n+ sudo -u ubuntu bash -lc 'test -d ~/myapp || git clone https://github.com/acme/myapp.git ~/myapp'\nCloning into '/home/ubuntu/myapp'...\nfatal: could not read Username for 'https://github.com': No such device or address\n+ true\n```\n\nUn repo **privado**, clonado sobre https pelón, sin credenciales. `|| true`\n\nse lo tragó, cloud-init reportó un arranque limpio, y la falla era\n\ninvisible a menos que alguien leyera el log.\n\nLas consecuencias se encadenaron. Sin repo, el `WorkingDirectory`\n\nde la unidad no existía, así que el servicio no podía arrancar aunque estuviera habilitado. No estaba habilitado, porque eso es un paso manual. Y el login del agente vivía solo en el volumen que se acababa de destruir:\n\n``` bash\n$ systemctl status agent-remote-control\n   Loaded: loaded (/etc/systemd/system/agent-remote-control.service; disabled; preset: enabled)\n   Active: inactive (dead)\n```\n\nSeis días después alguien fue a usarla.\n\n**Tres fallas independientes, una causa raíz: cada camino de recuperación requería un humano, y nada decía que se necesitaba un humano.**\n\nSi la caja no puede re-autenticarse sola, cada reemplazo es un outage. Dos secretos, sembrados una vez por cuenta en lugar de por instancia:\n\n``` php\n/myapp/agent-box-github-token        -> string de token pelón\n/myapp/agent-box-agent-credentials   -> blob JSON de credenciales\n```\n\nEl script de bootstrap jala los dos, autentica, clona, y habilita el\n\nservicio. Cada paso es idempotente, así que es seguro correrlo en cada arranque y en un timer.\n\n```\nlog \"1/5 GitHub auth\"\nif gh auth status >/dev/null 2>&1; then\n  log \"    already authenticated\"\nelse\n  GH_TOKEN_VALUE=\"$(secret \"$GH_SECRET\")\"\n  [ -n \"$GH_TOKEN_VALUE\" ] || fail \"could not read $GH_SECRET\"\n  printf '%s' \"$GH_TOKEN_VALUE\" | gh auth login --with-token || fail \"gh auth login failed\"\n  unset GH_TOKEN_VALUE\nfi\ngh auth setup-git >/dev/null 2>&1 || true\n```\n\nDos detalles no obvios:\n\n**El script no se puede descargar.** La jugada obvia es hacerle `curl`\n\ndesde el repo en el user-data. Ese repo es privado, y el token que autorizaría la descarga es lo que el script instala. Circular. En su lugar, incrusta el script en el user-data: léelo en tiempo de synth y mételo en base64.\n\n**El alcance de IAM es exactamente dos ARNs.** La caja obtiene `secretsmanager:GetSecretValue`\n\nsobre sus propios secretos de bootstrap y nada más. No tiene por qué leer el password de la base de datos.\n\n```\nrole.addToPolicy(new iam.PolicyStatement({\n  sid: \"ReadBootstrapSecrets\",\n  actions: [\"secretsmanager:GetSecretValue\"],\n  resources: [\n    `arn:aws:secretsmanager:${region}:${account}:secret:/myapp/agent-box-github-token-*`,\n    `arn:aws:secretsmanager:${region}:${account}:secret:/myapp/agent-box-agent-credentials-*`,\n  ],\n}));\n```\n\nRestaurar solo el token OAuth producía un servicio que arrancaba y se moría de inmediato:\n\n```\nError: Unable to determine your organization for Remote Control eligibility. Run `claude auth login` to refresh your account information.\n```\n\nLa funcionalidad de remote-control necesita contexto de cuenta, los\n\nidentificadores de la cuenta y de la organización, junto con el token.\n\nIncómodamente, en macOS estos viven en dos lugares distintos: el token en el keychain de login, el objeto de cuenta en el propio archivo de config del CLI. Una restauración que lee solo uno de los dos se ve completa y falla en runtime.\n\nGuarda los dos en un secreto y escríbelos a sus dos destinos en la caja:\n\n```\nif sec.get(\"oauthAccount\"):\n    d[\"oauthAccount\"] = sec[\"oauthAccount\"]\nif sec.get(\"organizationUuid\"):\n    d[\"organizationUuid\"] = sec[\"organizationUuid\"]\n```\n\nUn CLI de agente pregunta si debería confiar en un workspace en el primer uso dentro de un directorio. Un servicio de systemd no tiene TTY y no puede contestar, así que entra en crash-loop en el prompt. Pre-configura la bandera:\n\n```\ne = d.setdefault(\"projects\", {}).setdefault(workdir, {})\ne[\"hasTrustDialogAccepted\"] = True\ne[\"hasCompletedProjectOnboarding\"] = True\nd[\"hasCompletedOnboarding\"] = True\n```\n\nEn esta es fácil perder una hora, porque el texto del error es sobre trust y no dice nada de TTYs.\n\nLa primera versión del paso de contexto de cuenta hacía pipe de un secreto hacia Python mientras también usaba un heredoc para el script:\n\n```\n# ROTO\nsecret \"$CLAUDE_SECRET\" | python3 - \"$WORKDIR\" <<'TPY'\nsec = json.load(sys.stdin)\n```\n\nEl heredoc *es* stdin. El pipe se descarta, y Python lee su propio código fuente como el payload JSON:\n\n```\njson.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)\n```\n\nEscribe el secreto a un archivo `mktemp`\n\ncon `umask 077`\n\ny pasa la ruta como argumento.\n\n`Restart=always`\n\nmaneja el caso fácil: el proceso crasheó, arráncalo de nuevo.\n\n```\n[Unit]\nDescription=Agent remote-control session\nAfter=network-online.target\nWants=network-online.target\nStartLimitIntervalSec=0\n\n[Service]\nUser=appuser\nWorkingDirectory=/home/appuser/myapp\nEnvironment=PATH=/home/appuser/.local/bin:/usr/local/bin:/usr/bin:/bin\nExecStart=/home/appuser/.local/bin/claude remote-control --name myapp-cloud --continue\nRestart=always\nRestartSec=30\n\n[Install]\nWantedBy=multi-user.target\n```\n\n`StartLimitIntervalSec=0`\n\nimporta más de lo que parece. Por default systemd se rinde después de una ráfaga de fallas rápidas y estaciona la unidad en `failed`\n\n**permanentemente**. En una caja sin atender eso convierte un problema de red pasajero en un outage que dura hasta que alguien se da cuenta, que es justo el modo de falla que estamos diseñando para que no pase.\n\nPero `Restart=always`\n\nno puede ayudar con los estados que de verdad mataron esta caja: credenciales faltantes, un clone borrado, una unidad dejada deshabilitada. Esos necesitan algo fuera de la unidad. Como el script de bootstrap es idempotente, el watchdog es nada más el bootstrap en un timer:\n\n```\n[Unit]\nDescription=Watchdog for the agent session\n\n[Timer]\nOnBootSec=60\nOnCalendar=*:0/5\nAccuracySec=30s\nPersistent=true\n\n[Install]\nWantedBy=timers.target\n```\n\n`OnUnitActiveSec`\n\nen una unidad que nunca ha corrido\nEl primer timer usaba `OnUnitActiveSec=5min`\n\n. Se instaló limpio, reportó habilitado, y nunca disparó: ese setting es relativo a la última activación de la unidad, y una unidad que nunca ha corrido no tiene siguiente disparo:\n\n```\nNEXT  LEFT  LAST                          PASSED     UNIT\n-     -     Wed 2026-07-22 16:43:57 UTC   13ms ago   agent-bootstrap.timer\n```\n\n`NEXT`\n\nes `-`\n\n. Un watchdog que está inerte en silencio es peor que ninguno, porque se lee como cubierto. Usa `OnCalendar`\n\nde reloj de pared en su lugar.\n\nTodo lo de arriba acorta el outage. Nada de eso acorta los **seis días**, porque una recuperación de la que nunca te enteras es indistinguible de ninguna recuperación.\n\nEl watchdog ya corre cada cinco minutos y ya sabe si el servicio está sano.\n\nHaz que lo diga, en voz alta, a algún lugar fuera de la caja:\n\n```\nheartbeat() {\n  local status=\"$1\" detail=\"${2:-}\"\n  local stream ts\n  stream=\"$(cat /var/lib/cloud/data/instance-id 2>/dev/null || hostname)\"\n  ts=\"$(date +%s)000\"\n  aws logs create-log-stream --region \"$REGION\" --log-group-name \"$LOG_GROUP\" \\\n    --log-stream-name \"$stream\" >/dev/null 2>&1 || true\n  aws logs put-log-events --region \"$REGION\" --log-group-name \"$LOG_GROUP\" \\\n    --log-stream-name \"$stream\" \\\n    --log-events \"timestamp=$ts,message=AGENT_BOX_HEARTBEAT $status $detail\" \\\n    >/dev/null 2>&1 || log \"WARN: heartbeat not delivered\"\n}\n\nfail() { log \"ERROR: $*\"; heartbeat DOWN \"$*\"; exit 1; }\n```\n\nUn metric filter cuenta las líneas sanas, y la alarma dispara ante su\n\n**ausencia**:\n\n```\nnew logs.MetricFilter(this, \"HeartbeatMetricFilter\", {\n  logGroup,\n  metricNamespace: \"myapp/AgentBox\",\n  metricName: \"RemoteControlHeartbeat\",\n  filterPattern: logs.FilterPattern.literal('\"AGENT_BOX_HEARTBEAT OK\"'),\n  metricValue: \"1\",\n  defaultValue: 0,\n});\n\nnew cw.Alarm(this, \"RemoteControlDownAlarm\", {\n  alarmName: \"myapp-agent-box-down\",\n  metric: new cw.Metric({\n    namespace: \"myapp/AgentBox\",\n    metricName: \"RemoteControlHeartbeat\",\n    period: cdk.Duration.minutes(15),\n    statistic: \"Sum\",\n  }),\n  threshold: 1,\n  comparisonOperator: cw.ComparisonOperator.LESS_THAN_THRESHOLD,\n  evaluationPeriods: 1,\n  treatMissingData: cw.TreatMissingData.BREACHING,\n}).addAlarmAction(new cwactions.SnsAction(alarmTopic));\n```\n\n** treatMissingData: BREACHING es todo el diseño.** El instinto es alarmar sobre una línea de error, pero cada modo de falla en este postmortem produjo\n\n`NOT_BREACHING`\n\n.Dos detalles de apoyo. El heartbeat dispara también en el camino de falla, así que una corrida rota es tan visible como una sana: la alarma se indexa en la ausencia de `OK`\n\n, no en la presencia de `DOWN`\n\n. Y `activating`\n\ncuenta como sano, ya que es el estado normal por unos segundos después de cualquier reinicio:\n\n```\nSTATE=\"$(systemctl is-active agent-remote-control 2>/dev/null)\"\ncase \"$STATE\" in\n  active|activating) heartbeat OK \"$STATE\" ;;\n  *) fail \"service is $STATE\" ;;\nesac\n```\n\nCambia por el chequeo de `tmux has-session`\n\n+ `#{pane_dead}`\n\nde antes para una caja de Kiro. Todo lo de aguas abajo (log group, metric filter, alarma, topic de SNS) es idéntico, porque la línea de heartbeat es el único contrato entre la caja y la alarma.\n\nLa caja original vivía en el stack del backend de producción. Eso es lo que la mató: un parámetro de AMI dentro de *ese* stack reemplazó la instancia. Cada deploy de producción también la detenía/arrancaba.\n\nUna comodidad de desarrollo no tiene por qué compartir ciclo de vida con producción. Su propio stack, con un lookup de VPC en lugar de un export entre stacks, un export recrearía el acoplamiento que la separación existe para quitar:\n\n```\nexport class AgentBoxStack extends cdk.Stack {\n  constructor(scope: Construct, id: string, props: Props) {\n    super(scope, id, props);\n\n    const vpc = ec2.Vpc.fromLookup(this, \"Vpc\", { vpcName: \"myapp-vpc\" });\n\n    const alarmTopic = new sns.Topic(this, \"AgentBoxAlerts\", {\n      topicName: \"myapp-agent-box-alerts\",\n    });\n    alarmTopic.addSubscription(new snssub.EmailSubscription(\"ops@example.com\"));\n\n    new AgentBox(this, \"AgentBox\", {\n      appName: \"myapp\",\n      vpc,\n      githubRepo: \"acme/myapp\",\n      alarmTopic,\n    });\n  }\n}\n```\n\nSu propio topic de SNS, también. Importar el topic de producción metería el stack de vuelta justo en el grafo de dependencias.\n\n**El movimiento destruye y recrea la instancia**, y el orden no es opcional si algún recurso tiene un nombre físico fijo. El rol de IAM aquí lo tiene:\n\n```\nnpx cdk deploy MyApp-Backend    # borra la caja vieja y su rol de nombre fijo\nnpx cdk deploy MyApp-AgentBox   # los crea de nuevo\n```\n\nAl revés, el segundo deploy falla por un nombre de rol duplicado.\n\nLa separación del stack destruyó la instancia y construyó una nueva desde cero, lo que reproduce la falla original exactamente. Esta vez nadie la tocó:\n\n```\nMyApp-AgentBox | 12/13 | CREATE_COMPLETE | AWS::EC2::Instance | AgentBox/Instance\nMyApp-AgentBox | 13/13 | CREATE_COMPLETE | AWS::CloudFormation::Stack | MyApp-AgentBox\n ✅  MyApp-AgentBox\n✨  Deployment time: 177.81s\n```\n\nComo dos minutos después, sin atender:\n\n```\n[agent-box-bootstrap] 1/5 GitHub auth\n[agent-box-bootstrap] 2/5 Repo clone\n[agent-box-bootstrap] 3/5 Agent credentials\n[agent-box-bootstrap] 4/5 Account context + workspace trust\n[agent-box-bootstrap] 5/5 Enabling the always-on service\n[agent-box-bootstrap] done (active)\nsystemd[1]: Finished agent-bootstrap.service.\nsystemd[1]: agent-bootstrap.service: Consumed 2.954s CPU time, 52.5M memory peak\n```\n\nY la alarma hizo su trabajo a lo largo de toda la ventana. Durante el hueco del arranque:\n\n```\nThreshold Crossed: no datapoints were received for 1 period and 1 missing\ndatapoint was treated as [Breaching].    ALARM\n```\n\nluego, por su cuenta, en cuanto aterrizó el primer heartbeat:\n\n```\nThreshold Crossed: 1 datapoint [1.0 (22/07/26 20:40:00)] was not less than\nthe threshold (1.0).    OK\n```\n\nUna alarma que dispara ante un outage real y se limpia sola sin ayuda es todo el entregable. La recuperación está padre; la alarma es lo que convierte seis días en quince minutos.\n\n`|| true`\n\nes una decisión de nunca enterarte.`ImageId`\n\nlo destruye. La misma superficie de diff, un radio de impacto salvajemente distinto: lee el plan antes de desplegar.`NEXT`\n\ndel timer. El monitoreo inerte es peor que ninguno, porque se cuenta como cobertura.", "url": "https://wpnews.pro/news/sesiones-remotas-siempre-disponibles-para-tus-agentes-de-codigo", "canonical_source": "https://dev.to/aws-builders/sesiones-remotas-siempre-disponibles-para-tus-agentes-de-codigo-549f", "published_at": "2026-07-25 19:19:33+00:00", "updated_at": "2026-07-25 20:01:15.584010+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents", "ai-infrastructure"], "entities": ["Claude Code", "Kiro CLI", "AWS", "EC2", "Secrets Manager", "SSM", "tmux", "systemd"], "alternates": {"html": "https://wpnews.pro/news/sesiones-remotas-siempre-disponibles-para-tus-agentes-de-codigo", "markdown": "https://wpnews.pro/news/sesiones-remotas-siempre-disponibles-para-tus-agentes-de-codigo.md", "text": "https://wpnews.pro/news/sesiones-remotas-siempre-disponibles-para-tus-agentes-de-codigo.txt", "jsonld": "https://wpnews.pro/news/sesiones-remotas-siempre-disponibles-para-tus-agentes-de-codigo.jsonld"}}