You Don't Need a Framework to Build an AI Assistant
There is a tendency in the AI tooling space to reach for frameworks — LangChain, AutoGen, CrewAI, OpenClaw — the moment you want an AI that does more than answer one question at a time. Most of the time, that is the wrong move. The framework adds complexity, dependencies, and debugging surface area for problems that a few shell scripts and cron jobs solve perfectly well.
Claude Code’s headless mode (-p flag) plus a markdown file for personality plus cron scheduling is a complete AI assistant stack. Here is how to build it.
The Architecture
flowchart TD
Cron[Cron schedule] --> Script[Bash script]
Script --> Template[Load prompt template\nfrom prompts/]
Script --> Memory[Read memory files\nfrom memories/]
Script --> Claude[claude -p with combined prompt]
Claude --> Output[Write output to file]
Output --> Notify[Send to Telegram or Slack]
subgraph Assistant Directory
SOUL[SOUL.md\nPersonality and principles]
Memories[memories/\nPersistent context files]
Prompts[prompts/\nTask templates]
Scripts[scripts/\nCron-invoked shell scripts]
end
No server. No message queue. No framework. The entire system is files and shell scripts.
The SOUL.md File
The first thing to create is a personality file. This defines how the assistant approaches its work — its tone, its priorities, its operating principles. Without this, outputs feel generic.
# Assistant: Devika
## Role
You are Devika, a research and briefing assistant for a DevOps engineering team.
Your job is to save engineers time by surfacing relevant information before they know they need it.
## Operating Principles
- Be specific and technical — this audience is senior engineers, not beginners
- Cite sources — do not make claims without a URL or reference
- Be concise — a tight 200-word section beats a rambling 500-word one
- Surface actionable insights — "here is something you can try today" is more valuable than "here is an interesting trend"
- Do not repeat yourself across runs — check what was covered last time
## Tone
Professional but conversational. Not corporate. Not casual.
Write like a knowledgeable colleague who is good at summarising.
## Output Format
- Use Markdown headings
- Include a TL;DR at the top (3 bullet points maximum)
- Keep total length under 800 words
The Memory System
Memory is just markdown files in a directory. Each file captures a category of persistent context:
memories/
├── last_weekly.md ← previous weekly briefing (auto-updated)
├── topics_covered.md ← running list of covered topics
├── team_context.md ← what this team works on (static)
└── feedback.md ← lessons from past runs (manually updated)
The team_context.md is static — write it once:
# Team Context
This team runs a platform engineering function at a mid-size SaaS company.
Current focus areas: Kubernetes migration, Terraform module standardisation,
developer experience improvements.
Tools in use: AWS, Kubernetes (EKS), Terraform, GitHub Actions, PagerDuty, Datadog.
Stack: Node.js microservices, PostgreSQL, Redis.
Do not brief on topics already in regular team discussions.
Prompt Templates
Templates are markdown files with {{VARIABLE}} placeholders. Each task gets its own template:
# Weekly Engineering Briefing - {{DATE}}
Read the following context files before starting:
- Team context: {{TEAM_CONTEXT}}
- Last briefing: {{LAST_BRIEFING}}
- Topics already covered: {{TOPICS_COVERED}}
## Your Task
Research and write a briefing for the platform engineering team covering:
1. New releases in our toolchain (Kubernetes, Terraform, AWS services we use)
2. One notable incident post-mortem published this week — what happened and what we can learn
3. One practical technique or tool worth trying — include a concrete example
4. One community discussion worth reading — with a brief summary and why it matters
Constraints:
- Do not cover topics listed in {{TOPICS_COVERED}}
- Maximum 800 words total
- All claims must have sources
- Save a summary of topics covered to memories/topics_covered.md before finishing
The Shell Script
The script loads the soul, memory, and template — assembles them into a complete prompt — and runs claude -p:
#!/bin/bash
# scripts/weekly-brief.sh
set -e
BASE_DIR="$(cd "$(dirname "$0")/.." && pwd)"
DATE=$(date +%Y-%m-%d)
LOG="$BASE_DIR/logs/$(date +%Y-%m-%d).log"
log() { echo "[$(date +%H:%M:%S)] $1" | tee -a "$LOG"; }
log "Starting weekly briefing"
# Load memory files
TEAM_CONTEXT=$(cat "$BASE_DIR/memories/team_context.md" 2>/dev/null || echo "")
LAST_BRIEFING=$(cat "$BASE_DIR/memories/last_weekly.md" 2>/dev/null || echo "No previous briefing.")
TOPICS_COVERED=$(cat "$BASE_DIR/memories/topics_covered.md" 2>/dev/null || echo "None yet.")
# Assemble prompt from template
PROMPT=$(cat "$BASE_DIR/prompts/weekly-brief.md")
PROMPT="${PROMPT//\{\{DATE\}\}/$DATE}"
PROMPT="${PROMPT//\{\{TEAM_CONTEXT\}\}/$TEAM_CONTEXT}"
PROMPT="${PROMPT//\{\{LAST_BRIEFING\}\}/$LAST_BRIEFING}"
PROMPT="${PROMPT//\{\{TOPICS_COVERED\}\}/$TOPICS_COVERED}"
# Add the soul
FULL_PROMPT="$(cat "$BASE_DIR/SOUL.md")\n\n---\n\n$PROMPT"
# Run Claude
log "Running Claude"
RESULT=$(claude -p \
--dangerously-skip-permissions \
-d "$BASE_DIR" \
"$FULL_PROMPT")
# Save as this week's memory
echo "$RESULT" > "$BASE_DIR/memories/last_weekly.md"
log "Report saved to memories/last_weekly.md"
# Send to Telegram
if [ -n "$TELEGRAM_TOKEN" ] && [ -n "$TELEGRAM_CHAT_ID" ]; then
curl -s -X POST \
"https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage" \
-d "chat_id=${TELEGRAM_CHAT_ID}" \
-d "text=${RESULT:0:4000}" \
-d "parse_mode=Markdown"
log "Delivered to Telegram"
fi
log "Done"
Scheduling
# Create the log directory
mkdir -p /home/user/assistant/logs
# Edit crontab
crontab -e
# Weekly on Sunday at 8 AM
0 8 * * 0 /home/user/assistant/scripts/weekly-brief.sh
# Daily standup at 9 AM weekdays
0 9 * * 1-5 /home/user/assistant/scripts/daily-standup.sh
Why This Beats a Framework
flowchart LR
subgraph Framework[Framework approach]
F1[Install 50 dependencies]
F2[Learn framework API]
F3[Debug framework behaviour]
F4[Handle framework updates]
F5[Vendor lock-in]
end
subgraph Shell[Shell approach]
S1[3 shell scripts]
S2[Standard Unix tools]
S3[Transparent and auditable]
S4[No dependencies to update]
S5[Runs anywhere with bash]
end
Security advantage: Frameworks that accept inbound messages (Slack DM, email) are vulnerable to prompt injection — someone sends a message that tricks the assistant into doing something harmful. The shell script approach is outbound-only: cron triggers the script, which reads local files, which you control. No external input reaches the assistant during a run.
Transparency: Every step is a file you can read. The prompt is a markdown file. The memory is a markdown file. The output is a markdown file. There is nothing hidden in a framework’s internal state.
Portability: These scripts run on any machine with Claude Code installed. No Docker image, no framework version pinning, no cloud dependency.
Extending It
Once the weekly briefing works, the pattern extends to any scheduled task:
# Daily: What merged yesterday?
0 9 * * 1-5 scripts/daily-git-summary.sh
# After deploys: Write a deploy summary
# Triggered by CI webhook
scripts/deploy-summary.sh
# Monthly: Infrastructure cost report
0 8 1 * * scripts/monthly-costs.sh
Each script follows the same pattern: load context, build prompt, run claude -p, save output, notify. The soul and memory system stay shared across all scripts.
The most powerful thing about this approach is that you start with something that works in an afternoon and grow it incrementally. No framework design decisions to make upfront, no architecture to commit to. Just scripts that do one thing well.
