Reminders...

Systemd-run — Limitar Recursos

Introducción

systemd-run permite lanzar cualquier comando dentro de una unidad transitoria de systemd, es decir, una unidad que se crea al vuelo y desaparece cuando el proceso termina, sin necesidad de escribir un archivo .service. Como cada unidad tiene su propio cgroup, se le pueden aplicar las mismas directrices de control de recursos que a un servicio normal mediante la opción -p (--property).

Requisitos:

  • cgroups v2, que es lo predeterminado en cualquier distribución moderna. Se comprueba con stat -fc %T /sys/fs/cgroup, que debe devolver cgroup2fs.
  • Con --user el comando corre bajo el gestor de usuario, sin sudo, pero solo se pueden usar los controladores que systemd delega al usuario. Por defecto son cpu, memory y pids:
cat /sys/fs/cgroup/user.slice/user-$(id -u).slice/user@$(id -u).service/cgroup.controllers

Para delegar más controladores (por ejemplo cpuset para limitar núcleos o io para disco):

sudo systemctl edit user@.service
[Service]
Delegate=cpu cpuset io memory pids

Después hay que cerrar sesión y volver a entrar, o reiniciar.


Creando procesos como scope

systemd-run puede crear dos tipos de unidades transitorias:

Tipo Opción Comportamiento
.service Por defecto Lo lanza systemd en segundo plano, con un entorno limpio, y la salida va al journal
.scope --scope Lo lanza la propia terminal en primer plano, hereda el entorno y la salida se ve en la terminal

Para programas interactivos o gráficos lo más cómodo es --scope, ya que el programa se comporta exactamente igual que si se lanzara directamente, solo que dentro de su propio cgroup:

systemd-run --user --scope -p MemoryMax=2G firefox

Opciones útiles:

  • -u nombre / --unit=nombre: nombre de la unidad. Si no se indica, se genera uno tipo run-r3f2a....scope.
  • --slice=nombre.slice: agrupa varias unidades bajo un mismo slice, de modo que el límite se reparte entre todas.
  • -p se puede repetir tantas veces como propiedades se quieran aplicar.
  • -q / --quiet: no muestra el mensaje con el nombre de la unidad.
  • Solo para .service: --pty conecta la terminal al proceso, --wait espera a que termine y --collect descarga la unidad aunque falle.

Consultar el consumo y modificar los límites en caliente:

systemctl --user status firefox.scope    # Estado, consumo y límites
systemd-cgtop                            # Consumo por cgroup en tiempo real
systemctl --user set-property --runtime firefox.scope MemoryMax=1G

Para no escribir todo cada vez, se puede añadir una función al .zshrc / .bashrc:

limitar() { systemd-run --user --scope -q "$@"; }

limitar -p MemoryMax=1G -p CPUQuota=50% programa

Limitar CPU

Limitar uso de CPU

  • CPUQuota=: Límite duro de tiempo de CPU, en porcentaje de un núcleo: 50% es medio núcleo y 200% son dos núcleos completos. Aunque el sistema esté libre, el proceso nunca pasa de ese límite.
  • CPUWeight= : Prioridad relativa, de 1 a 10000 (por defecto 100). Solo tiene efecto cuando hay competencia por la CPU; si el sistema está libre, el proceso usa toda la que necesite. CPUWeight=idle le da la mínima prioridad posible.
  • --nice= : Ajusta el nice clásico (de -20 a 19). Los valores negativos requieren root.
# Compilar sin pasar de 2 núcleos
systemd-run --user --scope -p CPUQuota=200% make -j8

# Tarea de fondo que solo usa la CPU sobrante
systemd-run --user --scope -p CPUWeight=idle ffmpeg -i entrada.mkv salida.mp4

Limitar núcleos

A diferencia de CPUQuota, aquí no se limita cuánto tiempo de CPU usa el proceso sino en qué núcleos puede ejecutarse. Los núcleos disponibles se ven con lscpu -e. Hay tres formas:

  • AllowedCPUs= : Usa el controlador cpuset del cgroup. Acepta rangos y listas: 0-3, 0,2,4. Funciona con --scope, pero en modo --user requiere haber delegado cpuset (ver Introducción); si no, la propiedad simplemente se ignora.
  • CPUAffinity= : Usa sched_setaffinity(), por lo que no necesita cgroups ni delegación, pero solo funciona en unidades .service (sin --scope), ya que es systemd quien lanza el proceso.
  • taskset : Dentro de un scope, sin delegación ni servicios.
# Con cpuset delegado
systemd-run --user --scope -p AllowedCPUs=0-3 blender

# Sin delegación, como servicio
systemd-run --user --pty -p CPUAffinity=0-3 ./script.sh

# Sin delegación, como scope
systemd-run --user --scope taskset -c 0-3 blender

Para comprobar en qué núcleos corre un proceso: taskset -cp <PID>.


Limitar Ram

Directriz Descripción
MemoryMax= Límite duro. Si se supera, el OOM killer mata el proceso
MemoryHigh= Límite blando. Al superarlo el kernel ralentiza el proceso y lo obliga a liberar memoria (moviéndola a swap), pero no lo mata
MemorySwapMax= Swap máxima que puede usar. 0 le desactiva la swap
MemoryLow=, MemoryMin= Lo contrario: memoria protegida que el kernel intenta no reclamarle

Los valores aceptan sufijos K, M, G y T (base 1024), un porcentaje de la RAM física (MemoryMax=25%) o infinity.

Lo recomendable es combinar MemoryHigh un poco por debajo de MemoryMax, de manera que el proceso se ralentice antes de llegar al límite duro y que el OOM killer sea solo el último recurso:

systemd-run --user --scope -p MemoryHigh=3G -p MemoryMax=4G -p MemorySwapMax=1G chromium

Si se usa MemorySwapMax=0 junto a MemoryMax, al llegar al límite el proceso muere directamente sin pasar por la swap.


Limitar red

cgroups v2 no tiene controlador de red, así que systemd no puede limitar el ancho de banda, pero sí controlar a dónde puede conectarse un proceso. Estas directrices funcionan mediante BPF, por lo que requieren el gestor del sistema (sudo, sin --user); en el gestor de usuario no están disponibles. Para no ejecutar el programa como root se usa --uid:

Directriz Descripción
IPAddressDeny=any Bloquea todo el tráfico IP
IPAddressAllow= Excepciones a lo anterior: localhost, link-local, multicast, 192.168.1.0/24...
RestrictNetworkInterfaces= Solo permite ciertas interfaces, o las prohíbe si se antepone ~
PrivateNetwork=yes El proceso solo ve su propia interfaz lo. Solo en .service
IPAccounting=yes No limita, pero contabiliza los bytes enviados y recibidos (se ven en systemctl status)
# Sin acceso a internet, solo red local
sudo systemd-run --scope --uid=$USER \
  -p IPAddressDeny=any \
  -p IPAddressAllow=localhost \
  -p IPAddressAllow=192.168.1.0/24 \
  programa

# Que solo pueda salir por la VPN
sudo systemd-run --scope --uid=$USER -p "RestrictNetworkInterfaces=lo wg0" programa

RestrictNetworkInterfaces también afecta a lo, por eso hay que incluirlo; si no, deja de funcionar la resolución DNS a través de systemd-resolved (127.0.0.53).

Al pasar por sudo el entorno se resetea, por lo que las aplicaciones gráficas pueden perder DISPLAY / WAYLAND_DISPLAY. En ese caso hay que usar sudo -E o pasarlas con -E DISPLAY=$DISPLAY.

Ancho de banda

Para limitar la velocidad hay que recurrir a herramientas externas:

  • trickle: funciona en espacio de usuario mediante LD_PRELOAD, así que no sirve con binarios estáticos (por ejemplo, los compilados en Go). Las velocidades van en KB/s:
systemd-run --user --scope trickle -s -d 500 -u 100 programa
  • nftables + tc: la opción robusta. Se marcan los paquetes del cgroup con la expresión socket cgroupv2 de nftables y se limitan con tc. Es bastante más complejo de configurar.

Resumen

Recurso Propiedad --user --scope
Uso de CPU CPUQuota, CPUWeight Sí Sí
Núcleos AllowedCPUs Delegando cpuset Sí
Núcleos CPUAffinity Sí No
RAM MemoryMax, MemoryHigh, MemorySwapMax Sí Sí
Red IPAddressDeny, IPAddressAllow, RestrictNetworkInterfaces No, requiere sudo Sí
Red PrivateNetwork No, requiere sudo No

https://www.freedesktop.org/software/systemd/man/latest/systemd-run.html

https://www.freedesktop.org/software/systemd/man/latest/systemd.resource-control.html

Conectándose a Android usando MCP

Instalar Android-MCP en un venv y registrarlo en claude-code

Introducción

Android-MCP es un servidor MCP que permite a Claude operar un dispositivo Android como lo haría un usuario real: tocar la pantalla, escribir texto, hacer swipes, leer notificaciones y tomar snapshots de la interfaz.

La idea es simple: el teléfono queda conectado por ADB y el MCP expone esas acciones como herramientas. Desde claude-code se puede entonces pedir cosas como "abrí la app de mensajes y mandame el último chat" sin tocar el dispositivo.

Instalando Android-MCP

Lo primero es crear un entorno virtual aislado:

python3.13 -m venv ~/.venvs/android-mcp

Decidí usar la carpeta ~/.venvs/ para instalar el MCP porque así todos los servidores MCP quedan en un mismo lugar, cada uno en su propio venv. Nada se mezcla con el Python del sistema, no hay conflictos de dependencias entre servidores, y si algo se rompe basta con borrar la carpeta y volver a crearla. También hace que la ruta al binario sea predecible, que es justo lo que necesitamos después para registrarlo.

Luego instalamos el paquete (y actualizamos pip en el mismo paso):

~/.venvs/android-mcp/bin/pip install --upgrade pip android-mcp

Notar que se invoca el pip del venv por ruta absoluta, sin activarlo. Es menos pasos y evita olvidarse de que el entorno quedó activado.

Para testear que el MCP funciona:

~/.venvs/android-mcp/bin/android-mcp --help      # smoke test

Si devuelve la ayuda del comando, el binario está instalado y ejecutable. Ese es todo el smoke test que necesitamos antes de registrarlo.

Agregando el MCP a claude-code

claude mcp add android-mcp --scope user -- "$HOME/.venvs/android-mcp/bin/android-mcp"

Detalles de ese comando:

  • --scope user lo registra a nivel usuario, así está disponible en todos los proyectos y no sólo en el directorio actual.
  • El -- separa las opciones de claude mcp add del comando real a ejecutar.
  • Se usa $HOME en lugar de ~ porque dentro de las comillas la tilde no se expande.

Después de esto, reiniciar claude-code y las herramientas de Android deberían aparecer disponibles (ListDevices, ConnectDevice, Snapshot, Click, Type, Swipe, etc.). Un buen primer paso es pedir la lista de dispositivos para confirmar que el teléfono está visible por ADB.

Bludit en Agente de IA usando MCP

Introducción

Bludit guarda todo el contenido en archivos planos y trae un plugin de API que expone el blog por REST. Sobre esa API existe bludit-mcp, un servidor MCP (Model Context Protocol) que traduce esos endpoints en herramientas que un agente de IA puede llamar directamente.

El resultado es que desde la terminal, sin abrir el panel de administración, puedo pedirle al agente cosas como "busca el post que habla de ssh" o "pasa a markdown los posts que están escritos en HTML", y las ejecuta contra el blog. De hecho este mismo post fue creado así.

Hacen falta tres cosas: activar el plugin de API, sacar el token de autenticación del usuario admin y registrar el servidor MCP en Claude Code.

Activar Plugin de API

El plugin ya viene incluido en Bludit, pero está desactivado por defecto.

  1. Entrar al panel de administración y abrir Plugins.
  2. Buscar el plugin API y activarlo.
  3. Abrir sus Settings: ahí aparece el API Token.

Ese token es el de la instalación completa y hace falta en todas las peticiones, tanto de lectura como de escritura. Para comprobar que quedó funcionando:

curl "https://bludit_url.com/api/pages?token=api_token"

Ojo: si se reinstala el plugin, el API Token se regenera y hay que actualizarlo en el cliente.

Extraer API auth de usuario admin

El API Token solo alcanza para leer. Para crear, editar o borrar contenido la API pide además el token del usuario, que tiene que tener rol de Administrador:

Panel de administración → Manage → Users → admin → Security → Authentication Token

Las peticiones de escritura lo mandan en el campo authentication, junto al token:

{
  "token": "api_token",
  "authentication": "auth_token",
  "title": "Título de ejemplo"
}

Sin ese campo, o con un valor inválido, la API responde 401 Unauthorized.

Los dos tokens son credenciales y quedan guardados en texto plano en la configuración del cliente, así que no van a un repositorio.

Instalar Bludit-MCP en Claude-code:

claude mcp add bludit \
  -e BLUDIT_URL=https://bludit_url.com \
  -e BLUDIT_API_TOKEN=api_token \
  -e BLUDIT_AUTH_TOKEN=auth_token \
  -- npx -y bludit-mcp

Por defecto el servidor queda registrado solo para el proyecto actual; agregando -s user queda disponible en todas las sesiones.

Para verificar, claude mcp list desde la terminal, o el comando /mcp dentro de Claude Code, que muestra el estado de la conexión y las herramientas que expone.

Comandos de MCP incluidos en bludit

Comando Qué hace
list_pages Lista las páginas, con filtros por tipo (published, draft, static, sticky, scheduled), filtro untagged y paginación (pageNumber, numberOfItems, donde -1 devuelve todas). Responde también con total y hasMore.
get_page Devuelve una página por su key, que es el slug y puede ser jerárquico (padre/hijo).
create_page Crea una página. Solo title es obligatorio; content va en markdown y además acepta slug, tags, category, description, date, type, coverImage, parent, position y template. Si no se indica type, queda publicada.
edit_page Edita una página existente: el key más únicamente los campos que se quieren cambiar.
delete_page Borra la página con ese key.
get_website_settings Devuelve la configuración del sitio: nombre, descripción, idioma y demás ajustes.

Los tres comandos de escritura y get_website_settings necesitan el BLUDIT_AUTH_TOKEN; para listar y leer páginas basta con el BLUDIT_API_TOKEN.

Pass — Gestor de Contraseñas

pass, el gestor de contraseñas estándar de Unix: instalación, creación de la clave GPG, inicialización del almacén y sincronización con Git.

Introducción

Durante años usé una hoja de cálculo cifrada como gestor de contraseñas improvisado. Nunca quise recurrir a un servicio de terceros, porque delegar el almacenamiento de mis credenciales en una infraestructura ajena implica renunciar a la soberanía sobre ellas. Aun así, pospuse demasiado tiempo la migración a un sistema más consistente, más seguro y capaz de sincronizarse entre varios dispositivos.

El problema con KeePass

Mi primera opción fue KeePass: un formato abierto, auditado y con un ecosistema amplio de clientes. Sin embargo, tiene una limitación que me resultaba incómoda. La base de datos KDBX es un único contenedor cifrado de forma monolítica, de modo que cualquier modificación, por pequeña que sea, produce un archivo completamente distinto a nivel de bytes.

Eso tiene dos consecuencias. La primera es que, como el texto cifrado tiene una entropía prácticamente máxima, un sistema de control de versiones no puede calcular diferencias útiles entre revisiones y termina guardando una copia íntegra del archivo en cada commit. La segunda es que los cambios concurrentes desde dos dispositivos no se pueden fusionar: generan conflictos que hay que resolver a mano.

Por qué pass

Finalmente encontré pass, el gestor de contraseñas estándar de Unix, cuyo diseño me pareció especialmente elegante. Cada entrada es un archivo independiente cifrado con GPG dentro de una jerarquía de directorios, y la herramienta integra Git de forma nativa (pass git ...). Frente a un contenedor monolítico, las ventajas son varias:

  • Historial legible. Los cambios quedan aislados por entrada, así que el historial refleja exactamente qué credencial se modificó y cuándo.
  • Menos conflictos. Las ediciones simultáneas en entradas distintas se fusionan sin intervención.
  • Sincronización trivial. Se reduce a un push y un pull contra un repositorio remoto, que puede alojarse en un servidor propio, que es lo que hice.
  • Criptografía asimétrica. La seguridad descansa en claves GPG y la clave privada nunca necesita salir de los dispositivos del usuario.
  • Cliente para Android. Password Store trabaja contra el mismo repositorio y las mismas claves.

Instalar pass

En Debian y derivadas:

sudo apt install pass

En macOS, teniendo MacPorts instalado:

sudo port install pass

Claves GPG

pass no implementa criptografía propia: delega el cifrado en GPG (GNU Privacy Guard), que trabaja con criptografía asimétrica, es decir, con un par de claves pública/privada. La pública cifra cada entrada y la privada la descifra. Además, la clave privada se guarda cifrada con una passphrase, lo que añade un nivel extra de protección si alguien llega a tener acceso al disco.

Dónde se guardan

GPG mantiene todo su material en:

~/.gnupg/

Organizado de la siguiente manera:

~/.gnupg/
├── pubring.kbx               # Claves públicas (propias e importadas)
├── trustdb.gpg               # Nivel de confianza asignado a cada clave
├── private-keys-v1.d/        # Claves privadas, una por archivo
│   ├── A1B2C3...F9.key
│   └── D4E5F6...07.key
├── openpgp-revocs.d/         # Certificados de revocación
│   └── <FINGERPRINT>.rev
├── gpg.conf                  # Configuración de gpg
└── gpg-agent.conf            # Configuración de gpg-agent
  • pubring.kbx: contiene todas las claves públicas, tanto las propias como las importadas de otras personas, junto con sus identidades (nombre y correo), subclaves y firmas.
  • private-keys-v1.d/: guarda las claves privadas. Cada clave y cada subclave es un archivo independiente, nombrado por su keygrip. Están cifrados con la passphrase y los gestiona gpg-agent.
  • trustdb.gpg: almacena cuánto se confía en cada clave pública, es decir, si se cree que realmente pertenece a quien dice ser.
  • openpgp-revocs.d/: contiene un certificado de revocación por cada clave generada. Sirve para invalidar la clave si se pierde o queda comprometida.
  • gpg.conf y gpg-agent.conf: archivos de configuración opcionales, por ejemplo para definir algoritmos preferidos o cuánto tiempo se recuerda la passphrase.

Para ver qué claves hay y qué archivo corresponde a cada una:

gpg --list-keys                              # claves públicas
gpg --list-secret-keys --with-keygrip        # claves privadas y su keygrip

Cómo se identifican

Las claves se quedan en la carpeta personal del usuario, pero hay que referirse a ellas constantemente desde la línea de comandos. Para eso existen cuatro identificadores: el fingerprint, el key ID, el keygrip y el User ID.

  • Fingerprint (huella digital): es el identificador principal y único de la clave, un hash del material público de 40 caracteres hexadecimales (SHA-1) en claves v4 o de 64 (SHA-256) en claves v6. Por ejemplo: 3A1F 9C2E 7B4D 0A11 8E6C 5F2B 9D03 47AA C1E8 B205. Es la forma más segura de referirse a una clave.
  • Key ID: es el final del fingerprint. El long key ID son los últimos 16 caracteres (9D0347AAC1E8B205) y el short key ID, los últimos 8 (C1E8B205). El corto ya no se considera seguro, porque es fácil fabricar claves falsas que coincidan con él.
  • Keygrip: es un identificador interno que usa gpg-agent y que da nombre a los archivos de private-keys-v1.d/. A diferencia del fingerprint, solo depende del material criptográfico de la clave, no del formato OpenPGP.
  • User ID: es la identidad legible asociada a la clave, con la forma Nombre (comentario) <email>. Resulta cómodo en comandos como pass init usuario@ejemplo.com, pero no es único: cualquiera puede crear una clave con el nombre y el correo de otra persona.

Para ver todos estos identificadores a la vez:

gpg --list-keys --keyid-format long --with-fingerprint --with-keygrip

Crear la clave

El par de claves se genera en modo interactivo:

gpg --full-generate-key

GPG pregunta por cada detalle de la clave. A continuación, qué responder en cada paso.

1. Tipo de clave

gpg: directory '/Users/mac/.gnupg' created
Please select what kind of key you want:
   (1) RSA and RSA
   (2) DSA and Elgamal
   (3) DSA (sign only)
   (4) RSA (sign only)
   (9) ECC (sign and encrypt) *default*
  (10) ECC (sign only)
  (14) Existing key from card
  (16) ECC and Kyber
Your selection?

Elegí el tipo 9, que es el valor por defecto. ECC (Elliptic Curve Cryptography) genera una clave primaria de firma y certificación basada en EdDSA (Ed25519) y una subclave de cifrado basada en ECDH (Cv25519). Frente a RSA ofrece un nivel de seguridad equivalente con claves mucho más cortas (256 bits en ECC equivalen aproximadamente a 3072 bits en RSA), lo que se traduce en firmas más pequeñas y operaciones más rápidas.

2. Curva elíptica

Please select which elliptic curve you want:
   (1) Curve 25519 *default*
   (4) NIST P-384
   (6) Brainpool P-256
Your selection?

Elegí la opción 1, Curve 25519, que es la recomendada: está diseñada para resistir ataques de canal lateral (implementaciones de tiempo constante), no depende de constantes de origen opaco como las curvas NIST y está ampliamente soportada en GnuPG >= 2.1.

3. Periodo de validez

Please specify how long the key should be valid.
         0 = key does not expire
      <n>  = key expires in n days
      <n>w = key expires in n weeks
      <n>m = key expires in n months
      <n>y = key expires in n years
Key is valid for? (0)

Indiqué 0, es decir, sin vencimiento: no creo que lo necesite para este tipo de clave.

4. User ID

GnuPG needs to construct a user ID to identify your key.

Real name: Nombre Apellido
Email address: usuario@ejemplo.com
Comment:
You selected this USER-ID:
    "Nombre Apellido <usuario@ejemplo.com>"

Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? O

El campo Comment es opcional y conviene dejarlo vacío, por tres razones:

  • Forma parte del User ID, que queda ligado criptográficamente a la clave mediante la autofirma (self-signature) y por tanto no se puede editar. Para cambiarlo hay que crear un UID nuevo y revocar el anterior, y el UID revocado queda visible en los keyservers y en las copias de la clave pública que tengan otras personas.
  • Puede causar problemas de compatibilidad con herramientas que esperan el formato Nombre <email> y no Nombre (comentario) <email>.
  • Debilita la Web of Trust: quien certifica un UID no puede verificar el contenido del comentario, así que muchos usuarios evitan firmar los UID que lo incluyen.

5. Passphrase

Por último se pide la passphrase con la que se cifrará la clave privada en disco, en ~/.gnupg/private-keys-v1.d/. Para generarla uso el propio gpg, que devuelve 32 bytes aleatorios codificados en base64:

gpg --gen-random --armor 2 32

Resultado

Al terminar, GnuPG muestra un resumen parecido a este:

gpg: revocation certificate stored as '/Users/mac/.gnupg/openpgp-revocs.d/<FINGERPRINT>.rev'
public and secret key created and signed.

pub   ed25519 2026-10-11 [SC]
      <FINGERPRINT>
uid                      Nombre Apellido <usuario@ejemplo.com>
sub   cv25519 2026-10-11 [E]

[SC] indica que la clave primaria sirve para firmar (Sign) y certificar (Certify), y [E], que la subclave se usa para cifrar (Encrypt). No aparece ningún [expires: ...] porque la clave se creó sin fecha de vencimiento.

El certificado de revocación que GPG genera automáticamente conviene copiarlo a un lugar seguro fuera del equipo: es lo único que permite invalidar la clave si se pierde o queda comprometida.

Iniciar el almacén

Con la clave ya creada se puede inicializar el almacén de contraseñas, que vive en ~/.password-store (o en la ruta que indique la variable PASSWORD_STORE_DIR). Hay que pasarle el identificador de la clave con la que se cifrarán las entradas:

pass init <GPG-ID>

Esto crea el archivo ~/.password-store/.gpg-id, que guarda el identificador de la clave usada para cifrar cada entrada.

Sirve cualquiera de los cuatro identificadores:

pass init 3A1F9C2E7B4D0A118E6C5F2B9D0347AAC1E8B205   # fingerprint
pass init 9D0347AAC1E8B205                           # long ID
pass init C1E8B205                                   # short ID
pass init usuario@ejemplo.com                        # email / UID

Lo recomendable es usar el fingerprint completo, para evitar colisiones con otras claves, por ejemplo si dos comparten el mismo UID. Los short ID son especialmente problemáticos: con solo 32 bits, fabricar una clave que coincida con uno es trivial, un ataque conocido como Evil32.

Versionar el almacén con Git

Opcionalmente, el almacén se puede versionar con Git:

pass git init

Desde ese momento, pass crea un commit automático cada vez que se modifica el almacén. Para sincronizar basta añadir un remoto:

pass git remote add origin git@servidor:usuario/password-store.git
pass git push -u origin main

El nombre de la rama depende de la configuración init.defaultBranch de Git; en instalaciones antiguas será master.

A partir de ahí, la sincronización es la de cualquier repositorio:

pass git pull
pass git push

Cualquier subcomando de Git funciona anteponiendo pass git, ya que pass lo ejecuta dentro de ~/.password-store.

Como los archivos .gpg van cifrados, el remoto solo almacena texto cifrado. Aun así, los nombres de las entradas, que son las rutas de los archivos, quedan visibles en claro, así que conviene usar un repositorio privado.

Pass commands

Cada entrada se referencia por su ruta relativa a ~/.password-store, sin la extensión .gpg: pass show correo/trabajo descifra ~/.password-store/correo/trabajo.gpg. Los directorios son la única jerarquía que existe, así que basta con nombrar las entradas con barras para organizarlas. Si el almacén está versionado, todos los comandos que lo modifican crean su propio commit.

Listar y buscar

pass                        # muestra el árbol completo del almacén
pass ls correo              # muestra solo un subdirectorio
pass find trabajo           # busca en los nombres de las entradas
pass grep "ejemplo.com"     # busca dentro del contenido descifrado

find (alias search) solo mira los nombres de las entradas, que están en claro, así que es inmediato. grep descifra todas las entradas una por una para buscar en su contenido, por lo que pide la passphrase y tarda bastante más.

Ver una contraseña

pass show correo/trabajo    # imprime la entrada completa en el terminal
pass correo/trabajo         # show es el subcomando por defecto
pass -c correo/trabajo      # copia la contraseña al portapapeles
pass -c3 correo/trabajo     # copia la línea 3 en lugar de la primera
pass -q correo/trabajo      # muestra la contraseña como código QR

Con -c la contraseña no aparece nunca en pantalla y el portapapeles se limpia automáticamente a los 45 segundos (PASSWORD_STORE_CLIP_TIME). El código QR de -q necesita qrencode instalado y resulta cómodo para pasar una credencial al móvil.

Añadir entradas

pass insert correo/trabajo         # pide la contraseña dos veces, sin eco
pass insert -e correo/trabajo      # la pide una sola vez, mostrándola
pass insert -m correo/trabajo      # lee varias líneas hasta Ctrl-D
pass insert -f correo/trabajo      # sobrescribe sin preguntar

Por convención, la primera línea es la contraseña y el resto son metadatos de formato libre. Es lo que esperan pass -c, pass generate -i y los clientes como Password Store o Browserpass:

contraseña-super-secreta
user: mac
url: https://ejemplo.com

Generar contraseñas

pass generate correo/trabajo         # 25 caracteres, con símbolos
pass generate correo/trabajo 40      # longitud explícita
pass generate -n correo/trabajo      # solo alfanuméricos, sin símbolos
pass generate -c correo/trabajo      # además copia al portapapeles
pass generate -i correo/trabajo      # reemplaza solo la primera línea

La longitud por defecto es de 25 caracteres y se puede cambiar con PASSWORD_STORE_GENERATED_LENGTH. La opción -i (--in-place) es la que conviene al rotar una contraseña: sustituye la primera línea y conserva intactos los metadatos. Sin -i ni -f, pass pregunta antes de sobrescribir una entrada que ya existe.

Editar, mover y borrar

pass edit correo/trabajo                   # abre la entrada en $EDITOR
pass mv correo/trabajo trabajo/correo      # renombra o mueve
pass cp correo/trabajo correo/respaldo     # duplica
pass rm correo/trabajo                     # borra una entrada
pass rm -r correo                          # borra un directorio completo

pass edit descifra la entrada en un archivo temporal, lanza $EDITOR y la vuelve a cifrar al guardar. En Linux ese temporal se crea en /dev/shm, es decir, en memoria; en macOS, que no tiene /dev/shm, pass advierte de que el archivo en claro quedará en disco y pide confirmación antes de continuar.

mv y cp vuelven a cifrar la entrada cuando el destino está bajo un .gpg-id distinto, por ejemplo en un subdirectorio compartido con otras claves.

Variables de entorno

  • PASSWORD_STORE_DIR: ubicación del almacén. Por defecto, ~/.password-store.
  • PASSWORD_STORE_CLIP_TIME: segundos que la contraseña permanece en el portapapeles al usar -c. Por defecto, 45.
  • PASSWORD_STORE_GENERATED_LENGTH: longitud por defecto de pass generate. Por defecto, 25.
  • PASSWORD_STORE_UMASK: permisos con los que se crean los archivos del almacén.

Referencia completa

Los subcomandos de Git ya quedaron descritos en la sección anterior. Para el resto de opciones:

pass help
man pass

Negociación de USB Power Delivery en un Macbook

Introducción

Hace algunos años compré un cargador USB-C de 128 W para mi MacBook. Supuse que podía entregar los 128 W por un solo puerto, o repartir 65 W a cada uno de sus dos puertos cuando se usaran simultáneamente.

Con el tiempo noté una diferencia respecto al cargador original de Apple: cuando la batería se descargaba por completo, siempre tenía que esperar un rato con la laptop conectada antes de poder encenderla, mientras la batería recuperaba algo de carga. Eso me llevó a revisar cuánta potencia estaba recibiendo realmente el equipo.

Negociación USB Power Delivery

Al conectar el cable, el cargador (fuente) anuncia los perfiles de voltaje y corriente que puede ofrecer, y la laptop (consumidor) solicita uno de ellos. Este acuerdo, definido por el estándar USB Power Delivery (USB-PD), determina la potencia máxima que el cargador entregará.

Consultar la potencia negociada

macOS permite ver el resultado de esa negociación con el siguiente comando:

system_profiler SPPowerDataType | grep -A8 "AC Charger"

En mi caso, la salida fue:

AC Charger Information:
Connected: Yes
ID: 0x0000
Wattage (W): 65
Family: 0xe000400a
Charging: Yes

Consultar los detalles del adaptador y de la carga

Para obtener información más detallada se puede consultar el registro de I/O Kit:

ioreg -rw0 -c AppleSmartBattery | grep -E "AdapterDetails|AppleRawAdapterDetails|ChargerData"

Que en mi caso entregó:

"AdapterDetails" = {"IsWireless"=No,"AdapterID"=0,"AdapterVoltage"=20000,"FamilyCode"=18446744073172697098,"Watts"=65,"Current"=3250,"PMUConfiguration"=3144,"Description"="pd charger"}
"AppleRawAdapterDetails" = ({"IsWireless"=No,"AdapterID"=0,"AdapterVoltage"=20000,"FamilyCode"=18446744073172697098,"Watts"=65,"Current"=3250,"PMUConfiguration"=3144,"Description"="pd charger"})
"ChargerData" = {"ChargingCurrent"=3200,"NotChargingReason"=0,"ChargingVoltage"=12720,"VacVoltageLimit"=4240}

Los valores están expresados en mV y mA. De AdapterDetails se obtiene el contrato negociado con el cargador:

Potencia negociada (W):                                65
Voltaje negociado (V):                                 20
Corriente negociada (A):                               3.25
Límite de corriente de entrada configurado en el PMU (A): 3.144

El contrato es consistente: 20 V × 3.25 A = 65 W. El PMU limita la corriente de entrada a 3.144 A, aproximadamente un 3 % por debajo de lo negociado, como margen de seguridad.

De ChargerData se obtiene el estado de carga de la batería:

Corriente de carga (A):            3.2
Voltaje de carga (V):              12.72
Límite de voltaje por celda (V):   4.24

El voltaje de carga corresponde a una batería de tres celdas en serie (3 × 4.24 V = 12.72 V), y la potencia que se dirige a la batería es de unos 40.7 W (12.72 V × 3.2 A). El resto de los 65 W queda disponible para el funcionamiento del equipo y se pierde en parte en la conversión.

Conclusión

Al cambiar el cable USB-C de un puerto al otro del cargador, ambos negociaron la misma potencia máxima: 65 W. Es decir, los 128 W que anuncia el cargador corresponden a la capacidad total combinada de ambos puertos, no a lo que puede entregar un solo puerto.

Esto probablemente explica también la espera al encender con la batería agotada: si la potencia disponible por el puerto no alcanza a cubrir los picos de consumo del arranque, el equipo necesita recuperar un mínimo de carga en la batería antes de permitir el encendido, algo que no ocurre con un cargador que ofrece más potencia por puerto.

Buscar servidor usando NMAP

Introducción

En general, lo mejor para encontrar el servidor es asignarle una dirección IP estática. Pero hay casos en que esta no es la mejor opción. Por ejemplo, si el servidor se cambia constantemente de lugar y de red.

Anotar la dirección mac de la tarjeta de red

El primer paso es encontrar la dirección mac de la tarjeta de red, para esto simplemente se debe usar el comando:

ip link

Va a dar una salida como esta:

lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
      link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
no1: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc fq_codel state DOWN mode DEFAULT group default qlen 1000
      link/ether f4:4d:30:67:5b:21 brd ff:ff:ff:ff:ff:ff
      altname enp0s31f6
      altname enxf44d30675b21
wlp1s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DORMANT group default qlen 1000
      link/ether a0:c5:89:3d:c6:d8 brd ff:ff:ff:ff:ff:ff
      altname wlxa0c5893dc6d8

En mi caso estoy usando una tarjeta wifi, por voy a usar la dirección mac dada por la misma, que es a0:c5:89:3d:c6:d8

Encontrar el servidor

Para encontrar el servidor se debe usar nmap, que es una herramienta de escaneo de redes. El comando debe ejecutarse como root para que sea capaz de leer las direcciones mac de sus vecinos:

sudo nmap -sn -n 192.168.1.0/24
Starting Nmap 7.95 ( https://nmap.org ) at 2026-09-18 08:16 EDT
Nmap scan report for 192.168.1.68
Host is up (0.20s latency).
MAC Address: xx:xx:xx:xx:xx:xx (Tuya Smart)
Nmap scan report for 192.168.1.70
Host is up (0.20s latency).
MAC Address: xx:xx:xx:xx:xx:xx (Tuya Smart)
Nmap scan report for 192.168.1.71
Host is up (0.24s latency).
MAC Address: xx:xx:xx:xx:xx:xx (Tuya Smart)
Nmap scan report for 192.168.1.72
Host is up (0.24s latency).
MAC Address: xx:xx:xx:xx:xx:xx (Tuya Smart)
Nmap scan report for 192.168.1.76
Host is up (0.41s latency).
MAC Address: a0:c5:89:3d:c6:d8 (Intel)
Nmap scan report for 192.168.1.109
Host is up (0.41s latency).
MAC Address: xx:xx:xx:xx:xx:xx (Nest Labs)
Nmap scan report for 192.168.1.110
Host is up (0.081s latency).

Comando corto para encontrar servidor

Una opción corta para encontrar el servidor es combinarlo con grep y pedir que decuelva solo el

sudo nmap -sn -n 192.168.1.0/24 | grep -i -B 2 "a0:c5:89:3d:c6:d8"

Coviene crear un alias para el comando llamado findHomeServer o algo así para que sea más rápido ejecutar el comando en el futuro.